@intentic/constants 1.323.0 → 1.325.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/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export declare const PUBLIC_DIR = "public";
10
10
  export declare const MEMORY_FILE = "AGENTS.md";
11
11
  export declare const FIELD_NOTES_FILE = ".intentic/config/field-notes.toon";
12
12
  export declare const HOST_STATE_ROOT = "/opt/intentic";
13
- export declare const LEGAL_VERSION = "2026-09-29";
13
+ export declare const LEGAL_VERSION = "2026-10-04";
14
14
  export declare const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
15
15
  export declare const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
16
16
  export declare const LEGAL_ENTITY_COUNTRY = "Poland";
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export const PUBLIC_DIR = "public";
10
10
  export const MEMORY_FILE = "AGENTS.md";
11
11
  export const FIELD_NOTES_FILE = ".intentic/config/field-notes.toon";
12
12
  export const HOST_STATE_ROOT = "/opt/intentic";
13
- export const LEGAL_VERSION = "2026-09-29";
13
+ export const LEGAL_VERSION = "2026-10-04";
14
14
  export const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
15
15
  export const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
16
16
  export const LEGAL_ENTITY_COUNTRY = "Poland";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/constants",
3
- "version": "1.323.0",
3
+ "version": "1.325.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",
@@ -186,6 +186,38 @@ const queueArgs = (match, config) => [
186
186
  match.id,
187
187
  ];
188
188
 
189
+ // A test run that names what it runs: a word after the test verb that reads as a path, a test file or a filter
190
+ // (`pnpm --filter web test src/router/signIn.test.ts`, `bun test --conditions=x src/a.test.ts`, `vitest run tools/`,
191
+ // `pnpm --filter @intentic/sandbox test filter-stats`). The
192
+ // memory gate prices it as one worker over a few files (memory-room.mjs TARGETED_RUN_BYTES), not as a package's
193
+ // suite: measured 2026-09-26..10-02, 2,147 of 2,463 test runs named their files, and the gate held them a median 6.4 s
194
+ // for work that takes under one. A recursive or turbo fan-out is never targeted, whatever it names.
195
+ const TEST_VERB = /^(test|vitest|jest)$/u;
196
+ const FAN_OUT = /^(-r|--recursive|turbo|--workspaces?|-ws)$/u;
197
+ const PATHLIKE = /[/.]\p{L}|^\p{L}[\w-]*\.(test|spec)\b|\b(test|spec)s?\b/u;
198
+ const targetedRun = (invocation) => {
199
+ const words = invocation.slice(0, MATCH_LIMIT).split(/\s+/u).filter((word) => word !== "");
200
+ if (words.some((word) => FAN_OUT.test(word))) {
201
+ return false;
202
+ }
203
+ const verb = words.findIndex((word, index) => TEST_VERB.test(word.split("/").pop() ?? "") && (index > 0 || word !== "test"));
204
+ if (verb === -1) {
205
+ return false;
206
+ }
207
+ // A bare word (`filter-stats`) is a filter too, unless it may be the value of the flag before it (`--reporter dot`).
208
+ return words.slice(verb + 1).some((word, index, rest) => {
209
+ if (word.startsWith("-") || word.startsWith("@") || word === "run" || !/\p{L}/u.test(word)) {
210
+ return false;
211
+ }
212
+ const before = rest[index - 1] ?? "";
213
+ return PATHLIKE.test(word) || !(before.startsWith("-") && before !== "--" && !before.includes("="));
214
+ });
215
+ };
216
+
217
+ // queue-run's `--size` for a match: `targeted` for a test run aimed at named files, nothing for everything else.
218
+ const sizeArgs = (match, invocation) =>
219
+ ["vitest", "package-script"].includes(match.id) && targetedRun(invocation) ? ["--size", "targeted"] : [];
220
+
189
221
  // 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
190
222
  // lets it hold forever.
191
223
  const holdWarnSeconds = (maxHold) => (maxHold > 0 ? maxHold / 2 : undefined);
@@ -202,6 +234,8 @@ module.exports = {
202
234
  overridesOf,
203
235
  matchInvocation,
204
236
  queueArgs,
237
+ targetedRun,
238
+ sizeArgs,
205
239
  holdWarnSeconds,
206
240
  ruleById,
207
241
  };
@@ -67,5 +67,9 @@ export function mergeHeavyRules(overrides?: HeavyCommandOverrides): HeavyCommand
67
67
  export function overridesOf(full: Partial<HeavyCommandSettings> & { readonly rules?: readonly HeavyCommandRule[] | undefined }): HeavyCommandOverrides;
68
68
  export function matchInvocation(invocation: string, config: HeavyCommands, report?: (detail: string) => void): HeavyMatch | undefined;
69
69
  export function queueArgs(match: HeavyMatch, config: HeavyCommandSettings): string[];
70
+ // Whether a test invocation names the files it runs, for the memory gate's smaller price.
71
+ export function targetedRun(invocation: string): boolean;
72
+ // queue-run's `--size targeted` for a targeted test run under a test rule, else nothing.
73
+ export function sizeArgs(match: HeavyMatch, invocation: string): string[];
70
74
  export function holdWarnSeconds(maxHold: number): number | undefined;
71
75
  export function ruleById(config: HeavyCommands, id: string): HeavyCommandRule | undefined;
@@ -15,7 +15,7 @@
15
15
  const { spawnSync } = require("node:child_process");
16
16
  const { accessSync, constants, readFileSync, writeFileSync } = require("node:fs");
17
17
  const { getPriority, setPriority } = require("node:os");
18
- const { matchInvocation, queueArgs } = require("./heavy-rules.cjs");
18
+ const { matchInvocation, queueArgs, sizeArgs } = require("./heavy-rules.cjs");
19
19
 
20
20
  // The spec the daemon handed down, or undefined when this program is not under it or is already covered.
21
21
  const pendingSpec = (env = process.env) => {
@@ -92,13 +92,14 @@ const takeTurn = ({ program, args, command, env = process.env }) => {
92
92
  if (spec === undefined) {
93
93
  return false;
94
94
  }
95
- const match = matchInvocation([program, ...args].join(" "), spec.rules);
95
+ const invocation = [program, ...args].join(" ");
96
+ const match = matchInvocation(invocation, spec.rules);
96
97
  if (match === undefined) {
97
98
  return false;
98
99
  }
99
100
  env.INTENTIC_HEAVY_HELD = match.id;
100
101
  applyClass(spec.klass);
101
- const queue = spec.queue === true && typeof spec.queueRun === "string" && runnable(spec.queueRun) ? [spec.queueRun, ...queueArgs(match, spec.rules), "--"] : [];
102
+ const queue = spec.queue === true && typeof spec.queueRun === "string" && runnable(spec.queueRun) ? [spec.queueRun, ...queueArgs(match, spec.rules), ...sizeArgs(match, invocation), "--"] : [];
102
103
  const runner = spec.offload?.[match.id];
103
104
  if (runner !== undefined && typeof spec.offloadRun === "string") {
104
105
  const here = queue.length === 0 ? "" : `${queue.map(quote).join(" ")} `;
@@ -3,6 +3,9 @@ export type RoomWorkload = "agentRuntime" | "service" | "panel" | "install" | "c
3
3
 
4
4
  export const COST_BYTES: Readonly<Record<RoomWorkload, number>>;
5
5
  export const PERSON_RESERVE_BYTES: number;
6
+ export const TARGETED_RUN_BYTES: number;
7
+ // A toolchain command aimed at named files, priced at TARGETED_RUN_BYTES instead of its class's cost.
8
+ export type RoomSize = "targeted";
6
9
  export const TEST_PROCESS_BYTES: Readonly<{ fanOutWorker: number; standaloneWorker: number; typecheck: number; ceiling: number }>;
7
10
  export const STALL_PERCENT: number;
8
11
  export const RESERVATION_MS: number;
@@ -12,7 +15,7 @@ export const READING_FILES: readonly string[];
12
15
  export interface MemoryReading {
13
16
  // memory.high, else memory.max, else the machine's memory; undefined where nothing bounds it.
14
17
  readonly limitBytes: number | undefined;
15
- // Working set plus swap; undefined where nothing measures it.
18
+ // Working set plus swap; undefined where nothing measures it. Free memory is counted against its resident part.
16
19
  readonly usedBytes: number | undefined;
17
20
  // The swapped part of `usedBytes`; 0 when swap is off or unaccounted.
18
21
  readonly swapBytes: number;
@@ -50,8 +53,12 @@ export interface RoomAnswer extends RoomJudgement {
50
53
 
51
54
  export function readingFrom(read: (path: string) => string | undefined): MemoryReading;
52
55
  export function readReadingSync(): MemoryReading;
53
- export function needBytes(workload: RoomWorkload, attended: boolean): number;
54
- export function judge(reading: MemoryReading, request: { readonly workload: RoomWorkload; readonly attended: boolean; readonly reservedBytes?: number }): RoomJudgement;
56
+ export function costBytesOf(workload: RoomWorkload, size?: RoomSize): number;
57
+ export function needBytes(workload: RoomWorkload, attended: boolean, size?: RoomSize): number;
58
+ export function judge(
59
+ reading: MemoryReading,
60
+ request: { readonly workload: RoomWorkload; readonly attended: boolean; readonly reservedBytes?: number; readonly size?: RoomSize | undefined },
61
+ ): RoomJudgement;
55
62
  export function freeBytesOf(reading: MemoryReading): number | undefined;
56
63
  export function askRoom(options?: {
57
64
  readonly workload?: RoomWorkload;
@@ -60,6 +67,8 @@ export function askRoom(options?: {
60
67
  readonly socketPath?: string;
61
68
  readonly intervalMs?: number;
62
69
  readonly read?: () => MemoryReading;
70
+ readonly size?: RoomSize;
71
+ readonly pid?: number;
63
72
  }): Promise<RoomAnswer>;
64
73
  export function askFree(options?: { readonly socketPath?: string; readonly read?: () => MemoryReading }): Promise<{
65
74
  readonly freeBytes: number | undefined;
@@ -9,8 +9,12 @@
9
9
  // memory, never more than the machine's
10
10
  // used the working set (memory.current less the inactive file cache the kernel reclaims first) plus what was
11
11
  // pushed to swap; at a root cgroup, which has no memory.current, the machine's own used memory and swap
12
- // free limit − used, and never more than the machine itself has available (MemAvailable): a sandbox on a
13
- // shared machine cannot take memory the machine does not have, whatever its own limit says
12
+ // free limit − the resident part of used, and never more than the machine itself has available (MemAvailable): a
13
+ // sandbox on a shared machine cannot take memory the machine does not have, whatever its own limit says.
14
+ // Swap is not subtracted: the limit bounds resident pages only, and cold pages parked in swap leave that room
15
+ // free. Counting them held every heavy command for minutes on a sandbox with 8 GB available (measured
16
+ // 2026-09-26..10-02: 4.2 GiB of idle swap, 17 h of gate waits a week). Swap that is being paged back in shows
17
+ // as stall, below, which holds work whatever the bytes say.
14
18
  // stall memory PSI `full avg10`: the share of the last ten seconds in which everything waited on memory
15
19
  import { spawnSync } from "node:child_process";
16
20
  import { existsSync, readFileSync, realpathSync } from "node:fs";
@@ -34,6 +38,11 @@ export const COST_BYTES = Object.freeze({
34
38
  toolchain: GIB,
35
39
  });
36
40
 
41
+ // What a toolchain command aimed at named files takes: one bun worker over one test file, not a package's suite
42
+ // (measured 2026-10-02: a 63-test file ran in 16 ms well under 300 MiB). heavy-rules.cjs `targetedRun` decides it from
43
+ // the invocation, and queue-run passes it as `--size targeted`.
44
+ export const TARGETED_RUN_BYTES = 512 * MIB;
45
+
37
46
  // What one test or typecheck process holds at its peak, for the scripts that size a fan-out to the free memory
38
47
  // (_tools/scripts/verify/test-workers.mjs) and the ceiling that stops a runaway one (_tools/scripts/lib/memory-ceiling.mjs).
39
48
  export const TEST_PROCESS_BYTES = Object.freeze({
@@ -139,16 +148,20 @@ export const readReadingSync = () => readingFrom(readSync);
139
148
 
140
149
  const gib = (bytes) => `${(bytes / GIB).toFixed(1)} GiB`;
141
150
 
151
+ /** What one more of `workload` costs: its class's cost, or a targeted run's where `size` says so. */
152
+ export const costBytesOf = (workload, size) =>
153
+ workload === "toolchain" && size === "targeted" ? TARGETED_RUN_BYTES : (COST_BYTES[workload] ?? COST_BYTES.agentRuntime);
154
+
142
155
  /** What starting one more of `workload` needs free: its own cost, plus a person's turn when nobody is waiting on it. */
143
- export const needBytes = (workload, attended) => (COST_BYTES[workload] ?? COST_BYTES.agentRuntime) + (attended ? 0 : PERSON_RESERVE_BYTES);
156
+ export const needBytes = (workload, attended, size) => costBytesOf(workload, size) + (attended ? 0 : PERSON_RESERVE_BYTES);
144
157
 
145
158
  /**
146
159
  * The verdict for one more of `workload`: `run`, or, when the sandbox is short, `refuse` for work a person is waiting
147
160
  * on (they are told, and decide) and `wait` for work nobody is (it is held until there is room). `reservedBytes` is what
148
161
  * other admissions still hold off the reading. An unmeasurable sandbox has no opinion, and runs.
149
162
  */
150
- export const judge = (reading, { workload, attended, reservedBytes = 0 }) => {
151
- const need = needBytes(workload, attended);
163
+ export const judge = (reading, { workload, attended, reservedBytes = 0, size }) => {
164
+ const need = needBytes(workload, attended, size);
152
165
  const { limitBytes, usedBytes, swapBytes, stallPercent, availableBytes } = reading;
153
166
  const unreserved = freeBytesOf(reading);
154
167
  if (limitBytes === undefined || usedBytes === undefined || unreserved === undefined) {
@@ -171,12 +184,12 @@ export const judge = (reading, { workload, attended, reservedBytes = 0 }) => {
171
184
  return { verdict: "run", needBytes: need, freeBytes, reservedBytes };
172
185
  }
173
186
  // The machine, not the sandbox's own limit, is what ran out: saying "9 of 16 GiB used" would read as a wrong refusal.
174
- if (availableBytes !== undefined && availableBytes < limitBytes - usedBytes) {
187
+ if (availableBytes !== undefined && availableBytes < limitBytes - (usedBytes - swapBytes)) {
175
188
  const held = reservedBytes > 0 ? `, and ${gib(reservedBytes)} held for work that just started` : "";
176
189
  return shortOf(`Sandbox memory is low: the machine it runs on has ${gib(availableBytes)} available${held}`);
177
190
  }
178
191
  // Resident and swapped are named apart once paging starts: the limit bounds resident pages only, so their sum can
179
- // exceed it, and "19.2 GiB of 16.0 GiB used" reads as a bug.
192
+ // exceed it, and "19.2 GiB of 16.0 GiB used" reads as a bug. Free is counted against the resident part alone.
180
193
  const used =
181
194
  swapBytes > 0
182
195
  ? `${gib(usedBytes - swapBytes)} resident + ${gib(swapBytes)} swapped, against ${gib(limitBytes)}`
@@ -186,17 +199,26 @@ export const judge = (reading, { workload, attended, reservedBytes = 0 }) => {
186
199
  return shortOf(`Sandbox memory is low: ${used}${held}`, { limitBytes, residentBytes: usedBytes - swapBytes, swapBytes });
187
200
  };
188
201
 
189
- /** Free memory as the formula counts it, or undefined where nothing bounds or measures it. */
202
+ /** Free memory as the formula counts it (the limit less what is resident), or undefined where nothing bounds or measures it. */
190
203
  export const freeBytesOf = (reading) =>
191
204
  reading.limitBytes === undefined || reading.usedBytes === undefined
192
205
  ? undefined
193
- : Math.max(0, Math.min(reading.limitBytes - reading.usedBytes, reading.availableBytes ?? Number.POSITIVE_INFINITY));
206
+ : Math.max(
207
+ 0,
208
+ Math.min(reading.limitBytes - (reading.usedBytes - (reading.swapBytes ?? 0)), reading.availableBytes ?? Number.POSITIVE_INFINITY),
209
+ );
194
210
 
195
211
  // ---- asking the daemon, with the formula itself as the fallback ----
196
212
 
197
- const askSocket = (workload, waitSeconds, label, socketPath) =>
213
+ const askSocket = (workload, waitSeconds, label, socketPath, size, pid) =>
198
214
  new Promise((resolve) => {
199
215
  const query = new URLSearchParams({ class: workload, wait: String(waitSeconds), label });
216
+ if (size !== undefined) {
217
+ query.set("size", size);
218
+ }
219
+ if (pid !== undefined) {
220
+ query.set("pid", String(pid));
221
+ }
200
222
  const asked = request({ socketPath, path: `/room?${query}`, method: "GET", timeout: (waitSeconds + 15) * 1000 }, (response) => {
201
223
  let body = "";
202
224
  response.setEncoding("utf8");
@@ -266,24 +288,36 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
266
288
  /**
267
289
  * The daemon's verdict for one more of `workload`, waiting up to `waitSeconds` for room; where no daemon answers, the
268
290
  * same formula over the same files, polled every `intervalMs`. Never throws: every failure is an answer of `run`.
291
+ * `size: "targeted"` prices a toolchain command aimed at named files (TARGETED_RUN_BYTES); `pid` is the process the
292
+ * admitted work runs as, whose exit hands its reservation back at once instead of after RESERVATION_MS.
269
293
  */
270
- export const askRoom = async ({ workload = "toolchain", waitSeconds = 0, label = "command", socketPath = ROOM_SOCKET, intervalMs = 5_000, read = readReadingSync } = {}) => {
271
- const answered = await askSocket(workload, waitSeconds, label, socketPath);
294
+ export const askRoom = async ({
295
+ workload = "toolchain",
296
+ waitSeconds = 0,
297
+ label = "command",
298
+ socketPath = ROOM_SOCKET,
299
+ intervalMs = 5_000,
300
+ read = readReadingSync,
301
+ size,
302
+ pid,
303
+ } = {}) => {
304
+ const answered = await askSocket(workload, waitSeconds, label, socketPath, size, pid);
272
305
  if (answered !== undefined) {
273
306
  return { ...answered, source: "daemon" };
274
307
  }
275
308
  const startedAt = Date.now();
276
- let verdict = judge(read(), { workload, attended: false });
309
+ let verdict = judge(read(), { workload, attended: false, size });
277
310
  while (verdict.verdict !== "run" && Date.now() - startedAt < waitSeconds * 1000) {
278
311
  await sleep(intervalMs);
279
- verdict = judge(read(), { workload, attended: false });
312
+ verdict = judge(read(), { workload, attended: false, size });
280
313
  }
281
314
  return { ...verdict, waitedMs: Date.now() - startedAt, source: "formula" };
282
315
  };
283
316
 
284
- // `memory-room --class toolchain --wait 120 --label NAME` holds a command until there is room, printing why it waited,
285
- // and always exits 0: a gate that cannot read the sandbox must not be the reason a command did not run. `--json` prints
286
- // the answer instead; `--free` prints the free memory as the daemon counts it, for a caller that sizes itself to it.
317
+ // `memory-room --class toolchain --wait 120 --label NAME [--size targeted] [--pid PID]` holds a command until there is
318
+ // room, printing why it waited, and always exits 0: a gate that cannot read the sandbox must not be the reason a command
319
+ // did not run. `--json` prints the answer instead; `--free` prints the free memory as the daemon counts it, for a caller
320
+ // that sizes itself to it.
287
321
  const main = async (args) => {
288
322
  const option = (name, fallback) => {
289
323
  const at = args.indexOf(`--${name}`);
@@ -295,7 +329,15 @@ const main = async (args) => {
295
329
  }
296
330
  const waitSeconds = Math.max(0, Number(option("wait", "0")) || 0);
297
331
  const label = option("label", "command");
298
- const answer = await askRoom({ workload: option("class", "toolchain"), waitSeconds, label, intervalMs: Math.max(50, Number(option("interval-ms", "5000")) || 5000) });
332
+ const pid = Number(option("pid", ""));
333
+ const answer = await askRoom({
334
+ workload: option("class", "toolchain"),
335
+ waitSeconds,
336
+ label,
337
+ intervalMs: Math.max(50, Number(option("interval-ms", "5000")) || 5000),
338
+ size: option("size", undefined) === "targeted" ? "targeted" : undefined,
339
+ pid: Number.isInteger(pid) && pid > 1 ? pid : undefined,
340
+ });
299
341
  if (args.includes("--json")) {
300
342
  process.stdout.write(`${JSON.stringify(answer)}\n`);
301
343
  return;