@intentic/constants 1.364.0 → 1.366.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 +14 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
- package/src/dev-previews.d.mts +58 -0
- package/src/dev-previews.mjs +321 -0
- package/src/heavy/heavy-rules.cjs +128 -7
- package/src/heavy/heavy-rules.d.cts +24 -2
- package/src/heavy/heavy-turn.cjs +14 -11
- package/src/index.ts +6 -0
- package/src/memory-room.d.mts +5 -2
- package/src/memory-room.mjs +23 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# constants
|
|
2
2
|
|
|
3
|
-
The values several packages must agree on (ports, workspace paths, origins, install-script URLs,
|
|
3
|
+
The values several packages must agree on (ports, workspace paths, origins, install-script URLs, sign-in parameters, name limits, the hosted price ladder), defined once so no copy drifts.
|
|
4
4
|
|
|
5
5
|
```mermaid
|
|
6
6
|
flowchart LR
|
|
@@ -22,16 +22,27 @@ flowchart LR
|
|
|
22
22
|
- `./node` holds `repoRoot` and `packageRoot`, which find the monorepo by walking up to `pnpm-workspace.yaml`
|
|
23
23
|
instead of counting `../..`. The `paths` check refuses counted roots.
|
|
24
24
|
- The `.mjs` modules (`control-bytes`, `contract-shrink`, `assertion-measure`, `mirror-roots`, `test-suites`,
|
|
25
|
-
`vocabulary`, `ci-infra-steps`, `memory-room`, `allow`, `heavy-rules`) are plain JavaScript with `.d.mts` types, so
|
|
25
|
+
`vocabulary`, `ci-infra-steps`, `memory-room`, `allow`, `heavy-rules`, `dev-previews`) are plain JavaScript with `.d.mts` types, so
|
|
26
26
|
the checks and the verify scripts import them by path before any install or build, and the daemon applies the same
|
|
27
27
|
rule from the same file.
|
|
28
28
|
- `heavy-rules.cjs` is the one heavy-command table (which programs queue, their pools, max-holds and deadlines, and
|
|
29
29
|
the owner's overrides merged on top); `heavy-hook.cjs` (a `node --require` preload) and `heavy-exec.cjs` (behind the
|
|
30
30
|
sandbox's wrappers for native programs) judge a program by what it is as it starts. CommonJS, so the hook loads
|
|
31
|
-
on any Node an agent's project pins.
|
|
31
|
+
on any Node an agent's project pins. A rule may also match the program's working directory (`cwd`), name its memory
|
|
32
|
+
price (`size`), and be judged under a program that already holds a slot (`nests`): the editor's `vue-tsc` takes the
|
|
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.
|
|
35
|
+
- `memory-room`'s `SIZE_BYTES` prices a toolchain command by its measured shape instead of its class: `targeted` (a test
|
|
36
|
+
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).
|
|
32
38
|
- `memory-room` is the one formula for whether the sandbox has room (limit, used, free, stall, and what each workload
|
|
33
39
|
class costs). The daemon's resource budget judges by it, and it is also the `memory-room` command `queue-run` asks:
|
|
34
40
|
it asks the daemon's room socket, and applies the formula itself where no daemon answers.
|
|
41
|
+
- `dev-previews` is the record of a conversation's managed dev preview (its demo server): where the records live
|
|
42
|
+
(`/tmp/intentic-previews`), what makes one trustworthy (pid, start tick, conversation stamp, pid namespace), the cap
|
|
43
|
+
of three and the ten-minute idle bound, the conversation's own port in 47150-47199, and how a preview is stopped
|
|
44
|
+
with its reason kept for the next start. The demo's dev script writes the records, the server keeps its own
|
|
45
|
+
current, and the daemon's keeper and the `preview` command read them.
|
|
35
46
|
- `./allow` reads the two exception forms every check and guard suite honours: the site pragma
|
|
36
47
|
`// allow(<check>): <reason>` and the commit trailer `Allow: <check> — <reason>`.
|
|
37
48
|
- `WORKSPACE_ROOT` and `HISTORY_ROOT` are defaults only: a running daemon reads its real roots from config.
|
package/dist/index.d.ts
CHANGED
|
@@ -104,4 +104,6 @@ export declare const TRANSLATOR_PORT = 8789;
|
|
|
104
104
|
export declare const BROKER_PORT = 8790;
|
|
105
105
|
export declare const GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
|
|
106
106
|
export declare const GOOGLE_TOKEN_STORAGE_KEY = "intentic.gid.481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
|
|
107
|
+
export declare const SIGN_IN_CODE_LENGTH = 6;
|
|
108
|
+
export declare const SANDBOX_NAME_MAX = 60;
|
|
107
109
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAOnC,eAAO,MAAM,cAAc,UAAU,CAAC;AAGtC,eAAO,MAAM,YAAY,aAAa,CAAC;AAGvC,eAAO,MAAM,SAAS,cAAc,CAAC;AAIrC,eAAO,MAAM,mBAAmB,kCAAqC,CAAC;AAItE,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAczD,CAAC;AAKF,eAAO,MAAM,aAAa,SAAS,CAAC;AAGpC,eAAO,MAAM,UAAU,WAAW,CAAC;AAKnC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,gBAAgB,sCAAsC,CAAC;AAGpE,eAAO,MAAM,eAAe,kBAAkB,CAAC;AAG/C,eAAO,MAAM,aAAa,eAAe,CAAC;AAC1C,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAI1D,eAAO,MAAM,iBAAiB,uCAAuC,CAAC;AACtE,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAC7C,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAGtC,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAG5C,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAG9D,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAI3D,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAIrE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D,eAAO,MAAM,eAAe;;iBAClB,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,YAAY;;;iBACnC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,aAAa;;;iBAEtC,IAAI,EAAE,eAAe;iBAAE,IAAI,EAAE,iBAAiB;;;iBAC7C,IAAI,EAAE,mBAAmB;iBAAE,IAAI,EAAE,kBAAkB;;;iBAC/C,IAAI,EAAE,eAAe;iBAAE,IAAI,EAAE,iBAAiB;;;iBAChD,IAAI,EAAE,OAAO;iBAAE,IAAI,EAAE,SAAS;;;iBAC7B,IAAI,EAAE,WAAW;iBAAE,IAAI,EAAE,UAAU;;;iBACrC,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,WAAW;;;iBACjC,IAAI,EAAE,aAAa;iBAAE,IAAI,EAAE,YAAY;;;iBACzC,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,aAAa;;;iBAClC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,cAAc;;;iBAC9C,IAAI,EAAE,SAAS;iBAAE,IAAI,EAAE,aAAa;;;iBACjC,IAAI,EAAE,aAAa;iBAAE,IAAI,EAAE,cAAc;;;iBAC3C,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,YAAY;;;iBACjC,IAAI,EAAE,cAAc;iBAAE,IAAI,EAAE,aAAa;;;iBAGhD,IAAI,EAAE,MAAM;iBAAE,IAAI,EAAE,QAAQ;;;iBACzB,IAAI,EAAE,UAAU;iBAAE,IAAI,EAAE,SAAS;;CACoB,CAAC;AAEpE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,eAAe,CAAC;AAGzD,eAAO,MAAM,gBAAgB,QAAS,aAAa,KAAG,MAA+D,CAAC;AAGtH,eAAO,MAAM,iBAAiB,QAAS,aAAa,KAAG,MAA+D,CAAC;AAIvH,cAAc,gBAAgB,CAAC;AAI/B,eAAO,MAAM,WAAW,OAAO,CAAC;AAChC,eAAO,MAAM,YAAY,OAAO,CAAC;AAGjC,eAAO,MAAM,UAAU,OAAO,CAAC;AAG/B,eAAO,MAAM,eAAe,OAAO,CAAC;AAIpC,eAAO,MAAM,WAAW,OAAO,CAAC;AAGhC,eAAO,MAAM,gBAAgB,6EAA6E,CAAC;AAG3G,eAAO,MAAM,wBAAwB,0FAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -66,4 +66,6 @@ export const TRANSLATOR_PORT = 8789;
|
|
|
66
66
|
export const BROKER_PORT = 8790;
|
|
67
67
|
export const GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
|
|
68
68
|
export const GOOGLE_TOKEN_STORAGE_KEY = `intentic.gid.${GOOGLE_CLIENT_ID}`;
|
|
69
|
+
export const SIGN_IN_CODE_LENGTH = 6;
|
|
70
|
+
export const SANDBOX_NAME_MAX = 60;
|
|
69
71
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/constants",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.366.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",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"src/memory-room.d.mts",
|
|
30
30
|
"src/allow.mjs",
|
|
31
31
|
"src/allow.d.mts",
|
|
32
|
+
"src/dev-previews.mjs",
|
|
33
|
+
"src/dev-previews.d.mts",
|
|
32
34
|
"src/heavy/heavy-rules.cjs",
|
|
33
35
|
"src/heavy/heavy-rules.d.cts",
|
|
34
36
|
"src/heavy/heavy-turn.cjs",
|
|
@@ -83,6 +85,10 @@
|
|
|
83
85
|
"types": "./src/allow.d.mts",
|
|
84
86
|
"default": "./src/allow.mjs"
|
|
85
87
|
},
|
|
88
|
+
"./dev-previews": {
|
|
89
|
+
"types": "./src/dev-previews.d.mts",
|
|
90
|
+
"default": "./src/dev-previews.mjs"
|
|
91
|
+
},
|
|
86
92
|
"./heavy-rules": {
|
|
87
93
|
"types": "./src/heavy/heavy-rules.d.cts",
|
|
88
94
|
"default": "./src/heavy/heavy-rules.cjs"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Types for dev-previews.mjs, the records of long-lived dev previews (the demo's per-conversation dev servers).
|
|
2
|
+
export interface PreviewRecord {
|
|
3
|
+
readonly v: 1;
|
|
4
|
+
readonly conversation: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly pid: number;
|
|
7
|
+
readonly startTicks: string;
|
|
8
|
+
readonly pidNamespace: string;
|
|
9
|
+
readonly port: number;
|
|
10
|
+
readonly url: string;
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly log: string;
|
|
13
|
+
readonly startedAt: number;
|
|
14
|
+
readonly lastActiveAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EndedNote {
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
readonly at: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PREVIEWS_DIR_ENV: string;
|
|
23
|
+
export const DEFAULT_PREVIEWS_DIR: string;
|
|
24
|
+
export const OWNER_ENV: string;
|
|
25
|
+
export const RECORD_ENV: string;
|
|
26
|
+
export const PREVIEW_PORTS: { readonly first: number; readonly last: number };
|
|
27
|
+
export const PREVIEW_CAP: number;
|
|
28
|
+
export const PREVIEW_IDLE_MS: number;
|
|
29
|
+
export const TOUCH_EVERY_MS: number;
|
|
30
|
+
|
|
31
|
+
export function previewsDir(env?: Readonly<Record<string, string | undefined>>): string;
|
|
32
|
+
export function recordName(conversation: string, label: string): string;
|
|
33
|
+
export function recordPath(dir: string, conversation: string, label: string): string;
|
|
34
|
+
export function hostLabel(conversation: string): string;
|
|
35
|
+
export function startTicksOf(pid: number, procRoot?: string): string | undefined;
|
|
36
|
+
export function ownerOf(pid: number, procRoot?: string): string | undefined;
|
|
37
|
+
export function pidNamespaceOf(pid?: number | "self", procRoot?: string): string;
|
|
38
|
+
export function parseRecord(text: string): PreviewRecord | undefined;
|
|
39
|
+
export function recordIsOurs(record: PreviewRecord, procRoot?: string): boolean;
|
|
40
|
+
export function recordIsLive(record: PreviewRecord, procRoot?: string): boolean;
|
|
41
|
+
export function readRecords(dir?: string, procRoot?: string): PreviewRecord[];
|
|
42
|
+
export function writeRecord(dir: string, record: PreviewRecord): string;
|
|
43
|
+
export function touchRecord(path: string, at?: number): PreviewRecord | undefined;
|
|
44
|
+
export function removeRecord(dir: string, record: PreviewRecord): void;
|
|
45
|
+
export function noteStop(dir: string, record: PreviewRecord, reason: string): void;
|
|
46
|
+
export function stopReason(dir: string, record: PreviewRecord): string | undefined;
|
|
47
|
+
export function endedNote(dir: string, conversation: string, label: string): EndedNote | undefined;
|
|
48
|
+
export function clearEnded(dir: string, conversation: string, label: string): void;
|
|
49
|
+
export function overCap(records: readonly PreviewRecord[], cap?: number): PreviewRecord[];
|
|
50
|
+
export function idleRecords(records: readonly PreviewRecord[], now?: number, idleMs?: number): PreviewRecord[];
|
|
51
|
+
export function stopPreview(
|
|
52
|
+
dir: string,
|
|
53
|
+
record: PreviewRecord,
|
|
54
|
+
reason: string,
|
|
55
|
+
options?: { readonly graceMs?: number; readonly procRoot?: string },
|
|
56
|
+
): Promise<void>;
|
|
57
|
+
export function preferredPort(conversation: string): number;
|
|
58
|
+
export function freePreviewPort(taken?: ReadonlySet<number>, from?: number, isFree?: (port: number) => Promise<boolean>): Promise<number | undefined>;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// The record a long-lived dev preview leaves while it runs, so the sandbox can keep their number down. The demo's dev
|
|
2
|
+
// script starts one per conversation and writes its record (_site/demo/scripts/dev.mjs); the server itself refreshes
|
|
3
|
+
// the record while it is used and stops itself once it is not (_site/demo/vite.config.ts, previewKeeper); the daemon
|
|
4
|
+
// shields a recorded server from the sweep that ends a finished turn's processes, and stops one that outlived its own
|
|
5
|
+
// idle stop (_sandbox/sandbox/src/processes/dev-previews.ts); the `preview` command lists and stops them
|
|
6
|
+
// (_sandbox/sandbox/bin/preview). Plain JavaScript with no dependencies, so all of them read one definition of where
|
|
7
|
+
// the records live, what makes one trustworthy, and how a preview is stopped.
|
|
8
|
+
//
|
|
9
|
+
// A record names a process, and the keepers signal it, so a record alone proves nothing: the process must still be
|
|
10
|
+
// alive, must have started at the clock tick the record says (a reused pid fails that), and must carry the
|
|
11
|
+
// conversation stamp (INTENTIC_TURN_OWNER) the record claims. A record written from another pid namespace (an agent
|
|
12
|
+
// domain has its own) names a pid this reader cannot see, so it is neither trusted nor removed here.
|
|
13
|
+
import { chmodSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { createServer } from "node:net";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export const PREVIEWS_DIR_ENV = "INTENTIC_PREVIEWS_DIR";
|
|
18
|
+
// /tmp, not the workspace: every conversation's mount namespace and the daemon see the same /tmp (the background
|
|
19
|
+
// jobs' run directories live there for the same reason), and nothing here should ever ride a land.
|
|
20
|
+
export const DEFAULT_PREVIEWS_DIR = "/tmp/intentic-previews";
|
|
21
|
+
export const OWNER_ENV = "INTENTIC_TURN_OWNER";
|
|
22
|
+
// Set on a server the dev script started, naming its record, so the server can keep it current.
|
|
23
|
+
export const RECORD_ENV = "INTENTIC_PREVIEW_RECORD";
|
|
24
|
+
// Ports the previews pick from: clear of 47145 and 47146, which the editor mirrors from the owner's own machine (a
|
|
25
|
+
// server there may be serving the owner's tree, not the conversation's).
|
|
26
|
+
export const PREVIEW_PORTS = Object.freeze({ first: 47150, last: 47199 });
|
|
27
|
+
// How many previews run at once across the sandbox, and how long one may go unused before it stops. One demo server
|
|
28
|
+
// holds 0.8 GiB warm and 2 GiB while it optimizes (measured 2026-10-27); five to seven of them, each up for a whole turn
|
|
29
|
+
// and idle most of it, were 9-13 GiB of a 21 GiB sandbox on 2026-10-26.
|
|
30
|
+
export const PREVIEW_CAP = 3;
|
|
31
|
+
export const PREVIEW_IDLE_MS = 10 * 60_000;
|
|
32
|
+
// How often a running server refreshes its record's lastActiveAt while it is used.
|
|
33
|
+
export const TOUCH_EVERY_MS = 30_000;
|
|
34
|
+
// How long a signalled preview gets to exit before SIGKILL.
|
|
35
|
+
const TERM_GRACE_MS = 5_000;
|
|
36
|
+
const RECORD_SUFFIX = ".json";
|
|
37
|
+
const STOP_SUFFIX = ".stop";
|
|
38
|
+
const ENDED_SUFFIX = ".ended";
|
|
39
|
+
|
|
40
|
+
export const previewsDir = (env = process.env) => env[PREVIEWS_DIR_ENV] || DEFAULT_PREVIEWS_DIR;
|
|
41
|
+
|
|
42
|
+
// One record per conversation and label: a second start in the same conversation finds the first.
|
|
43
|
+
export const recordName = (conversation, label) => `${conversation}--${label}`.replaceAll(/[^A-Za-z0-9._-]/gu, "_");
|
|
44
|
+
export const recordPath = (dir, conversation, label) => join(dir, `${recordName(conversation, label)}${RECORD_SUFFIX}`);
|
|
45
|
+
const stopPath = (dir, conversation, label) => join(dir, `${recordName(conversation, label)}${STOP_SUFFIX}`);
|
|
46
|
+
const endedPath = (dir, conversation, label) => join(dir, `${recordName(conversation, label)}${ENDED_SUFFIX}`);
|
|
47
|
+
|
|
48
|
+
// A conversation id as one DNS label, for its preview's hostname: `<label>.demo.localhost`.
|
|
49
|
+
export const hostLabel = (conversation) =>
|
|
50
|
+
conversation
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replaceAll(/[^a-z0-9-]+/gu, "-")
|
|
53
|
+
.replaceAll(/^-+|-+$/gu, "")
|
|
54
|
+
.slice(0, 63) || "preview";
|
|
55
|
+
|
|
56
|
+
// Field 22 of /proc/<pid>/stat, the process's start time in clock ticks since boot. The command name (field 2) can
|
|
57
|
+
// hold spaces and parentheses, so fields are counted after its closing parenthesis.
|
|
58
|
+
export const startTicksOf = (pid, procRoot = "/proc") => {
|
|
59
|
+
try {
|
|
60
|
+
const stat = readFileSync(join(procRoot, String(pid), "stat"), "utf8");
|
|
61
|
+
return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
|
|
62
|
+
} catch {
|
|
63
|
+
// allow(silent-catch): no such process (or no /proc) is the answer "not running".
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// The conversation stamp a process started with, from its initial environment.
|
|
69
|
+
export const ownerOf = (pid, procRoot = "/proc") => {
|
|
70
|
+
try {
|
|
71
|
+
const environ = readFileSync(join(procRoot, String(pid), "environ"), "utf8");
|
|
72
|
+
const entry = environ.split("\0").find((line) => line.startsWith(`${OWNER_ENV}=`));
|
|
73
|
+
return entry === undefined ? undefined : entry.slice(OWNER_ENV.length + 1);
|
|
74
|
+
} catch {
|
|
75
|
+
// allow(silent-catch): unreadable means unowned, and an unowned process is never one a record may name.
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Which pid namespace a process (default: this one) is in, as the kernel names it (`pid:[4026532293]`).
|
|
81
|
+
export const pidNamespaceOf = (pid = "self", procRoot = "/proc") => {
|
|
82
|
+
try {
|
|
83
|
+
return readlinkSync(join(procRoot, String(pid), "ns", "pid"));
|
|
84
|
+
} catch {
|
|
85
|
+
// allow(silent-catch): no procfs is one namespace nobody can tell apart, named as such.
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// A preview's address is loopback: the IP itself, or a name under `.localhost`, which browsers resolve there.
|
|
91
|
+
const LOOPBACK_URL = /^http:\/\/(127\.0\.0\.1|[a-z0-9-]+(\.[a-z0-9-]+)*\.localhost):\d+\//u;
|
|
92
|
+
|
|
93
|
+
const isPort = (port) => Number.isSafeInteger(port) && port > 0 && port < 65_536;
|
|
94
|
+
// What each field of a record must be once coerced.
|
|
95
|
+
const RECORD_RULES = [
|
|
96
|
+
(record) => record.v === 1 && record.conversation !== "" && record.label !== "",
|
|
97
|
+
(record) => Number.isSafeInteger(record.pid) && record.pid > 1 && /^\d+$/u.test(record.startTicks),
|
|
98
|
+
(record) => isPort(record.port) && LOOPBACK_URL.test(record.url),
|
|
99
|
+
(record) => Number.isFinite(record.startedAt) && Number.isFinite(record.lastActiveAt),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// A record as read off disk, or undefined for anything that is not one: every field coerced to its type, then held to
|
|
103
|
+
// what it means (a positive pid, digits for the start tick, a port in range, a loopback URL).
|
|
104
|
+
export const parseRecord = (text) => {
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = JSON.parse(text) ?? {};
|
|
108
|
+
} catch {
|
|
109
|
+
// allow(silent-catch): a half-written or foreign file is not a record.
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const record = {
|
|
113
|
+
v: Number(raw.v),
|
|
114
|
+
conversation: String(raw.conversation ?? ""),
|
|
115
|
+
label: String(raw.label ?? ""),
|
|
116
|
+
pid: Number(raw.pid),
|
|
117
|
+
startTicks: String(raw.startTicks ?? ""),
|
|
118
|
+
pidNamespace: String(raw.pidNamespace ?? ""),
|
|
119
|
+
port: Number(raw.port),
|
|
120
|
+
url: String(raw.url ?? ""),
|
|
121
|
+
root: String(raw.root ?? ""),
|
|
122
|
+
log: String(raw.log ?? ""),
|
|
123
|
+
startedAt: Number(raw.startedAt),
|
|
124
|
+
lastActiveAt: Number(raw.lastActiveAt ?? raw.startedAt),
|
|
125
|
+
};
|
|
126
|
+
return RECORD_RULES.every((holds) => holds(record)) ? record : undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Whether a record was written from this reader's pid namespace, so its pid means the process it meant.
|
|
130
|
+
export const recordIsOurs = (record, procRoot = "/proc") => record.pidNamespace === pidNamespaceOf("self", procRoot);
|
|
131
|
+
|
|
132
|
+
// Whether the process a record names is the one that wrote it, still running.
|
|
133
|
+
export const recordIsLive = (record, procRoot = "/proc") =>
|
|
134
|
+
startTicksOf(record.pid, procRoot) === record.startTicks && ownerOf(record.pid, procRoot) === record.conversation;
|
|
135
|
+
|
|
136
|
+
// Written whole and renamed into place, so a reader never sees half a file.
|
|
137
|
+
const writeWhole = (path, text) => {
|
|
138
|
+
const temporary = `${path}.${String(process.pid)}.tmp`;
|
|
139
|
+
writeFileSync(temporary, text);
|
|
140
|
+
renameSync(temporary, path);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// A dead record's stop note becomes its ended note: why it stopped, kept for the next start or listing to say.
|
|
144
|
+
const retire = (dir, record) => {
|
|
145
|
+
const reason = stopReason(dir, record);
|
|
146
|
+
if (reason !== undefined) {
|
|
147
|
+
writeWhole(endedPath(dir, record.conversation, record.label), `${JSON.stringify({ reason, at: Date.now() })}\n`);
|
|
148
|
+
}
|
|
149
|
+
removeRecord(dir, record);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Every live record in the directory written from this pid namespace; a file that does not parse is removed, and so is
|
|
153
|
+
// one of ours naming a process that is gone or not its own (its stop note kept as the ended note).
|
|
154
|
+
export const readRecords = (dir = previewsDir(), procRoot = "/proc") => {
|
|
155
|
+
let names;
|
|
156
|
+
try {
|
|
157
|
+
names = readdirSync(dir);
|
|
158
|
+
} catch {
|
|
159
|
+
// allow(silent-catch): no directory yet is no previews.
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const namespace = pidNamespaceOf("self", procRoot);
|
|
163
|
+
const records = [];
|
|
164
|
+
for (const name of names.filter((one) => one.endsWith(RECORD_SUFFIX))) {
|
|
165
|
+
const path = join(dir, name);
|
|
166
|
+
let record;
|
|
167
|
+
try {
|
|
168
|
+
record = parseRecord(readFileSync(path, "utf8"));
|
|
169
|
+
} catch {
|
|
170
|
+
// allow(silent-catch): a file removed between the listing and the read is gone either way.
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (record === undefined) {
|
|
174
|
+
rmSync(path, { force: true });
|
|
175
|
+
} else if (record.pidNamespace !== namespace) {
|
|
176
|
+
continue;
|
|
177
|
+
} else if (recordIsLive(record, procRoot)) {
|
|
178
|
+
records.push(record);
|
|
179
|
+
} else {
|
|
180
|
+
retire(dir, record);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return records.toSorted((a, b) => a.startedAt - b.startedAt);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const writeRecord = (dir, record) => {
|
|
187
|
+
mkdirSync(dir, { recursive: true });
|
|
188
|
+
// Sticky and open to all, like /tmp: a conversation in an agent domain writes here as a different uid.
|
|
189
|
+
try {
|
|
190
|
+
chmodSync(dir, 0o1777);
|
|
191
|
+
} catch {
|
|
192
|
+
// allow(silent-catch): a directory someone else made is theirs to keep open; writing below still says if it is not.
|
|
193
|
+
}
|
|
194
|
+
const path = recordPath(dir, record.conversation, record.label);
|
|
195
|
+
writeWhole(path, `${JSON.stringify(record)}\n`);
|
|
196
|
+
rmSync(stopPath(dir, record.conversation, record.label), { force: true });
|
|
197
|
+
rmSync(endedPath(dir, record.conversation, record.label), { force: true });
|
|
198
|
+
return path;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// The record with a later lastActiveAt, written in place; a record removed meanwhile (the preview was stopped) stays
|
|
202
|
+
// removed.
|
|
203
|
+
export const touchRecord = (path, at = Date.now()) => {
|
|
204
|
+
let record;
|
|
205
|
+
try {
|
|
206
|
+
record = parseRecord(readFileSync(path, "utf8"));
|
|
207
|
+
} catch {
|
|
208
|
+
// allow(silent-catch): no record is nothing to touch.
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (record === undefined) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const touched = { ...record, lastActiveAt: Math.max(record.lastActiveAt, at) };
|
|
215
|
+
writeWhole(path, `${JSON.stringify(touched)}\n`);
|
|
216
|
+
return touched;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const removeRecord = (dir, record) => {
|
|
220
|
+
rmSync(recordPath(dir, record.conversation, record.label), { force: true });
|
|
221
|
+
rmSync(stopPath(dir, record.conversation, record.label), { force: true });
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Why the sandbox is stopping a preview, written before the signal, so whoever looks next can say why it stopped.
|
|
225
|
+
export const noteStop = (dir, record, reason) => {
|
|
226
|
+
writeWhole(stopPath(dir, record.conversation, record.label), `${reason}\n`);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const stopReason = (dir, record) => {
|
|
230
|
+
try {
|
|
231
|
+
return readFileSync(stopPath(dir, record.conversation, record.label), "utf8").trim() || undefined;
|
|
232
|
+
} catch {
|
|
233
|
+
// allow(silent-catch): no note means nobody but the preview's own caller stopped it.
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Why this conversation's last preview of `label` stopped, when the sandbox stopped it: { reason, at }.
|
|
239
|
+
export const endedNote = (dir, conversation, label) => {
|
|
240
|
+
try {
|
|
241
|
+
const note = JSON.parse(readFileSync(endedPath(dir, conversation, label), "utf8")) ?? {};
|
|
242
|
+
const ended = { reason: String(note.reason ?? ""), at: Number(note.at) };
|
|
243
|
+
return ended.reason !== "" && Number.isFinite(ended.at) ? ended : undefined;
|
|
244
|
+
} catch {
|
|
245
|
+
// allow(silent-catch): no note is a preview nobody stopped, or one already reported.
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const clearEnded = (dir, conversation, label) => rmSync(endedPath(dir, conversation, label), { force: true });
|
|
251
|
+
|
|
252
|
+
// Which previews to stop so that one more fits under `cap`: the least recently used first.
|
|
253
|
+
export const overCap = (records, cap = PREVIEW_CAP) =>
|
|
254
|
+
records.toSorted((a, b) => a.lastActiveAt - b.lastActiveAt).slice(0, Math.max(0, records.length - cap + 1));
|
|
255
|
+
|
|
256
|
+
// Which previews have gone unused past the idle bound, judged by their record alone.
|
|
257
|
+
export const idleRecords = (records, now = Date.now(), idleMs = PREVIEW_IDLE_MS) => records.filter((record) => now - record.lastActiveAt >= idleMs);
|
|
258
|
+
|
|
259
|
+
const signalGroup = (pid, signal) => {
|
|
260
|
+
try {
|
|
261
|
+
process.kill(-pid, signal);
|
|
262
|
+
return true;
|
|
263
|
+
} catch {
|
|
264
|
+
try {
|
|
265
|
+
process.kill(pid, signal);
|
|
266
|
+
return true;
|
|
267
|
+
} catch {
|
|
268
|
+
// allow(silent-catch): already gone, which is what stopping it was for.
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
275
|
+
|
|
276
|
+
// Stops a preview: the reason noted first, then SIGTERM to its process group (a preview leads its own), SIGKILL once
|
|
277
|
+
// the grace is up, and its record retired so the reason is what remains. Resolves once it has exited.
|
|
278
|
+
export const stopPreview = async (dir, record, reason, { graceMs = TERM_GRACE_MS, procRoot = "/proc" } = {}) => {
|
|
279
|
+
noteStop(dir, record, reason);
|
|
280
|
+
if (signalGroup(record.pid, "SIGTERM")) {
|
|
281
|
+
const deadline = Date.now() + graceMs;
|
|
282
|
+
while (recordIsLive(record, procRoot) && Date.now() < deadline) {
|
|
283
|
+
await delay(100);
|
|
284
|
+
}
|
|
285
|
+
if (recordIsLive(record, procRoot)) {
|
|
286
|
+
signalGroup(record.pid, "SIGKILL");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
retire(dir, record);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Whether a port is free to listen on at 127.0.0.1, by trying it.
|
|
293
|
+
const portFree = (port) =>
|
|
294
|
+
new Promise((resolve) => {
|
|
295
|
+
const probe = createServer();
|
|
296
|
+
probe.once("error", () => resolve(false));
|
|
297
|
+
probe.listen({ host: "127.0.0.1", port, exclusive: true }, () => probe.close(() => resolve(true)));
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// The port a conversation's preview asks for first: the same one every time, so its origin (and the sign-in and look
|
|
301
|
+
// stored under it) survives a restart of its server.
|
|
302
|
+
export const preferredPort = (conversation) => {
|
|
303
|
+
let hash = 0;
|
|
304
|
+
for (const char of conversation) {
|
|
305
|
+
hash = (hash * 31 + char.codePointAt(0)) % 1_000_003;
|
|
306
|
+
}
|
|
307
|
+
return PREVIEW_PORTS.first + (hash % (PREVIEW_PORTS.last - PREVIEW_PORTS.first + 1));
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// The first free port in the preview range, starting at `from` and wrapping, that no live record claims; undefined
|
|
311
|
+
// when all are taken.
|
|
312
|
+
export const freePreviewPort = async (taken = new Set(), from = PREVIEW_PORTS.first, isFree = portFree) => {
|
|
313
|
+
const span = PREVIEW_PORTS.last - PREVIEW_PORTS.first + 1;
|
|
314
|
+
for (let step = 0; step < span; step += 1) {
|
|
315
|
+
const port = PREVIEW_PORTS.first + ((from - PREVIEW_PORTS.first + step) % span);
|
|
316
|
+
if (!taken.has(port) && (await isFree(port))) {
|
|
317
|
+
return port;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
};
|
|
@@ -17,6 +17,11 @@ const QUEUE_SKIPPED_EXIT_CODE = 75;
|
|
|
17
17
|
// Characters of an invocation a rule's regex runs against; bounds backtracking, since node has no regex timeout.
|
|
18
18
|
const MATCH_LIMIT = 4096;
|
|
19
19
|
|
|
20
|
+
// `vue-tsc` as the program, not a word joined to it (`check-vue-tsc`).
|
|
21
|
+
const VUE_TSC = "(?<![-.])\\bvue-tsc\\b(?![-.])";
|
|
22
|
+
// The two packages whose typecheck compiles the editor's whole source, by package name or by path.
|
|
23
|
+
const EDITOR_PACKAGE = "(@intentic/(web|demo)|_editor/web|_site/demo)(?![-\\w])";
|
|
24
|
+
|
|
20
25
|
// The shipped rules and settings. Every rule is something measured pinning a sandbox, not merely slow.
|
|
21
26
|
const SHIPPED_HEAVY_COMMANDS = Object.freeze({
|
|
22
27
|
// How many matching programs may run at once across the sandbox; two fits twice the peak of one bounded run.
|
|
@@ -61,6 +66,48 @@ const SHIPPED_HEAVY_COMMANDS = Object.freeze({
|
|
|
61
66
|
pool: "install",
|
|
62
67
|
limit: 1,
|
|
63
68
|
},
|
|
69
|
+
// The editor's typecheck, one at a time in a lane of its own, priced at what it holds (memory-room.mjs
|
|
70
|
+
// SIZE_BYTES): `vue-tsc` over `_editor/web` peaks at 6.8 GiB of its 8 GiB heap, and `_site/demo`'s check compiles
|
|
71
|
+
// the same source (measured 2026-10-26: 418-571 s each). Two at once, priced at a toolchain's 1 GiB, were the
|
|
72
|
+
// 20 GiB peaks of 2026-10-26, holding both heavy slots for 13-29 minutes while ten commands queued behind them.
|
|
73
|
+
// `nests`: judged again under a program that already holds a slot of a pool no `nests` rule names, so the
|
|
74
|
+
// `vue-tsc` a `pnpm -r typecheck` or a repository verification starts still takes this lane (see matchInvocation
|
|
75
|
+
// for why that cannot deadlock).
|
|
76
|
+
// - A changed-files check (`lsp diag` on a Vue package, _search/lsp/src/vue-check.ts) names the tsconfig it
|
|
77
|
+
// writes, `tsconfig.lsp-changed.<pid>.json`, and is priced as what it loads (measured 2026-10-27: 1.0-3.9 GiB,
|
|
78
|
+
// 5-55 s). A lane of its own, two at once, so it never waits behind a full check's ten minutes; the memory gate
|
|
79
|
+
// still prices each one.
|
|
80
|
+
// - The program, or the package script, naming the editor or the demo: `vue-tsc -p tsconfig.app.json` (only the
|
|
81
|
+
// editor has one), `pnpm --filter @intentic/web typecheck`, `turbo run typecheck --filter=@intentic/demo`.
|
|
82
|
+
// Never two at once, not even after the wait, for the reason `repo-verify` gives.
|
|
83
|
+
// - The same program or script run from inside either package, which its arguments cannot say (`cwd`).
|
|
84
|
+
{
|
|
85
|
+
id: "vue-typecheck-changed",
|
|
86
|
+
pattern: `${VUE_TSC}.*\\btsconfig\\.lsp-changed\\b`,
|
|
87
|
+
pool: "vue-typecheck-changed",
|
|
88
|
+
limit: 2,
|
|
89
|
+
size: "scoped-typecheck",
|
|
90
|
+
nests: true,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "editor-typecheck",
|
|
94
|
+
pattern: `${VUE_TSC}.*(\\btsconfig\\.app\\.json\\b|${EDITOR_PACKAGE})|^(pnpm|npm|yarn|bun|turbo)\\b(?=.*${EDITOR_PACKAGE})(?=.*\\btypecheck\\b)`,
|
|
95
|
+
pool: "vue-typecheck",
|
|
96
|
+
limit: 1,
|
|
97
|
+
size: "editor-typecheck",
|
|
98
|
+
nests: true,
|
|
99
|
+
onDeadline: "skip",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "editor-typecheck-here",
|
|
103
|
+
pattern: `^${VUE_TSC}|^(pnpm|npm|yarn|bun)\\b.*\\btypecheck\\b`,
|
|
104
|
+
cwd: "(^|/)(_editor/web|_site/demo)/?$",
|
|
105
|
+
pool: "vue-typecheck",
|
|
106
|
+
limit: 1,
|
|
107
|
+
size: "editor-typecheck",
|
|
108
|
+
nests: true,
|
|
109
|
+
onDeadline: "skip",
|
|
110
|
+
},
|
|
64
111
|
// A program's name joined to `.` or `-` is another word (`check-tsc`), not the program.
|
|
65
112
|
{ id: "vitest", pattern: "(?<![-.])\\b(vitest|jest)\\b(?![-.])" },
|
|
66
113
|
{ id: "typechecker", pattern: "(?<![-.])\\b(tsc|tsgo|vue-tsc)\\b(?![-.])" },
|
|
@@ -90,7 +137,10 @@ const EARLIER_SHIPPED_RULES = Object.freeze([
|
|
|
90
137
|
]);
|
|
91
138
|
|
|
92
139
|
const SETTING_KEYS = Object.freeze(["limit", "defaultPool", "waitSeconds", "memoryGateSeconds", "maxHoldSeconds", "onDeadline"]);
|
|
93
|
-
|
|
140
|
+
// `cwd`: a regex the program's working directory must match too, for what its arguments cannot name. `size`: the
|
|
141
|
+
// memory gate's price for it (memory-room.mjs SIZE_BYTES) instead of its class's. `nests`: judged even under a program
|
|
142
|
+
// already holding a slot of another pool.
|
|
143
|
+
const RULE_KEYS = Object.freeze(["id", "pattern", "pool", "limit", "maxHoldSeconds", "exempt", "onDeadline", "cwd", "size", "nests"]);
|
|
94
144
|
|
|
95
145
|
// Two rules are the same when every field either names is equal; key order and absent-versus-undefined do not count.
|
|
96
146
|
const sameRule = (left, right) => RULE_KEYS.every((key) => left[key] === right[key]);
|
|
@@ -150,28 +200,93 @@ const compileRule = (rule, report) => {
|
|
|
150
200
|
}
|
|
151
201
|
};
|
|
152
202
|
|
|
203
|
+
// A rule's `cwd` against the program's working directory: absent matches anywhere, and one that does not compile or a
|
|
204
|
+
// directory nobody named matches nothing.
|
|
205
|
+
const inDirectory = (rule, cwd, report) => {
|
|
206
|
+
if (rule.cwd === undefined) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (cwd === undefined) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
return new RegExp(rule.cwd, "i").test(cwd.slice(0, MATCH_LIMIT));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
report?.(`${rule.id}: cwd ${error instanceof Error ? error.message : "bad pattern"}`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// The pools some `nests` rule queues into: a program holding a slot of one of them is never judged again.
|
|
221
|
+
const nestingPools = (config) => new Set(config.rules.filter((rule) => rule.nests === true && rule.exempt !== true).map((rule) => rule.pool ?? config.defaultPool));
|
|
222
|
+
|
|
223
|
+
// Whether a rule is judged at all for a program whose ancestors already hold slots of `held`. Nothing held: every rule.
|
|
224
|
+
// Under a held slot, an `exempt` rule still ends the search (a watch is no less long-lived for running under a
|
|
225
|
+
// verification), and a `nests` rule is judged only while every held pool is one no `nests` rule names.
|
|
226
|
+
//
|
|
227
|
+
// WHY THIS CANNOT DEADLOCK. A program waits for a slot while holding others only through a `nests` rule, so every wait
|
|
228
|
+
// that holds something goes from a pool no `nests` rule names to one that a `nests` rule names, and never from a nesting
|
|
229
|
+
// pool to anything. Waits-for edges between pools all point one way, so no cycle of them exists, across sessions too;
|
|
230
|
+
// and a program never waits on a pool it holds, which is the flock on its own ancestor's slot that would never be free.
|
|
231
|
+
// The owner's overrides cannot break this: a `nests` rule they add makes its pool a nesting pool, whose holders are then
|
|
232
|
+
// never judged again.
|
|
233
|
+
const judgedUnder = (rule, held, nesting) => {
|
|
234
|
+
if (held.length === 0 || rule.exempt === true) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return rule.nests === true && held.every((pool) => !nesting.has(pool));
|
|
238
|
+
};
|
|
239
|
+
|
|
153
240
|
// The rule a program's invocation falls under, with what the queue does with it; undefined for one that is not heavy.
|
|
154
|
-
|
|
241
|
+
// `where.cwd` is the program's working directory, for a rule with a `cwd`; `where.heldPools` the pools programs above
|
|
242
|
+
// this one already hold slots of (heldPoolsOf), which narrows the rules judged (judgedUnder).
|
|
243
|
+
const matchInvocation = (invocation, config, report, where = {}) => {
|
|
155
244
|
const text = invocation.slice(0, MATCH_LIMIT);
|
|
245
|
+
const held = where.heldPools ?? [];
|
|
246
|
+
const nesting = held.length === 0 ? new Set() : nestingPools(config);
|
|
156
247
|
for (const rule of config.rules) {
|
|
248
|
+
if (!judgedUnder(rule, held, nesting)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
157
251
|
const compiled = compileRule(rule, report);
|
|
158
|
-
if (compiled === undefined || !compiled.regex.test(text)) {
|
|
252
|
+
if (compiled === undefined || !compiled.regex.test(text) || !inDirectory(rule, where.cwd, report)) {
|
|
159
253
|
continue;
|
|
160
254
|
}
|
|
161
255
|
if (rule.exempt === true) {
|
|
162
256
|
return undefined;
|
|
163
257
|
}
|
|
164
|
-
|
|
258
|
+
const match = {
|
|
165
259
|
id: rule.id,
|
|
166
260
|
pool: rule.pool ?? config.defaultPool,
|
|
167
261
|
limit: rule.limit ?? config.limit,
|
|
168
262
|
maxHold: rule.maxHoldSeconds ?? config.maxHoldSeconds,
|
|
169
263
|
onDeadline: rule.onDeadline ?? config.onDeadline,
|
|
170
264
|
};
|
|
265
|
+
if (rule.size !== undefined) {
|
|
266
|
+
match.size = rule.size;
|
|
267
|
+
}
|
|
268
|
+
return match;
|
|
171
269
|
}
|
|
172
270
|
return undefined;
|
|
173
271
|
};
|
|
174
272
|
|
|
273
|
+
// The pools programs above this one hold slots of, from what they handed down: the rule the nearest was queued under
|
|
274
|
+
// (INTENTIC_HEAVY_HELD, by its id in the merged table) and the slot queue-run took last (INTENTIC_QUEUE_SLOT,
|
|
275
|
+
// `<queue dir>/<pool>/slot.<n>`), which differ when a script queued itself under a queued program. Empty for a program
|
|
276
|
+
// nothing above has queued.
|
|
277
|
+
const heldPoolsOf = (env, config) => {
|
|
278
|
+
const pools = new Set();
|
|
279
|
+
const held = env.INTENTIC_HEAVY_HELD ?? "";
|
|
280
|
+
if (held !== "") {
|
|
281
|
+
pools.add(ruleById(config, held)?.pool ?? config.defaultPool);
|
|
282
|
+
}
|
|
283
|
+
const slot = env.INTENTIC_QUEUE_SLOT ?? "";
|
|
284
|
+
if (slot !== "") {
|
|
285
|
+
pools.add(slot.split("/").filter((part) => part !== "").at(-2) ?? config.defaultPool);
|
|
286
|
+
}
|
|
287
|
+
return [...pools];
|
|
288
|
+
};
|
|
289
|
+
|
|
175
290
|
// queue-run's flags for a match (bin/queue-run), before its `--`.
|
|
176
291
|
const queueArgs = (match, config) => [
|
|
177
292
|
"--pool",
|
|
@@ -218,9 +333,14 @@ const targetedRun = (invocation) => {
|
|
|
218
333
|
});
|
|
219
334
|
};
|
|
220
335
|
|
|
221
|
-
// queue-run's `--size` for a match: `targeted` for a test run aimed at named
|
|
222
|
-
|
|
223
|
-
|
|
336
|
+
// queue-run's `--size` for a match: its rule's own price where it names one, `targeted` for a test run aimed at named
|
|
337
|
+
// files, nothing for everything else.
|
|
338
|
+
const sizeArgs = (match, invocation) => {
|
|
339
|
+
if (match.size !== undefined) {
|
|
340
|
+
return ["--size", match.size];
|
|
341
|
+
}
|
|
342
|
+
return ["vitest", "package-script"].includes(match.id) && targetedRun(invocation) ? ["--size", "targeted"] : [];
|
|
343
|
+
};
|
|
224
344
|
|
|
225
345
|
// 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
|
|
226
346
|
// lets it hold forever.
|
|
@@ -242,4 +362,5 @@ module.exports = {
|
|
|
242
362
|
sizeArgs,
|
|
243
363
|
holdWarnSeconds,
|
|
244
364
|
ruleById,
|
|
365
|
+
heldPoolsOf,
|
|
245
366
|
};
|
|
@@ -10,6 +10,12 @@ export interface HeavyCommandRule {
|
|
|
10
10
|
readonly maxHoldSeconds?: number | undefined;
|
|
11
11
|
readonly exempt?: boolean | undefined;
|
|
12
12
|
readonly onDeadline?: OnDeadline | undefined;
|
|
13
|
+
// A regex the program's working directory must match too, for what its arguments cannot name.
|
|
14
|
+
readonly cwd?: string | undefined;
|
|
15
|
+
// The memory gate's price by name (memory-room.mjs SIZE_BYTES) instead of its class's; an unknown name is the class.
|
|
16
|
+
readonly size?: string | undefined;
|
|
17
|
+
// Judged even under a program already holding a slot, while every held pool is one no `nests` rule names.
|
|
18
|
+
readonly nests?: boolean | undefined;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
// An owner's change to the table: a shipped rule's id changes the fields it names, or removes it (`disabled`); any other
|
|
@@ -22,6 +28,12 @@ export interface HeavyCommandRuleEdit {
|
|
|
22
28
|
readonly maxHoldSeconds?: number | undefined;
|
|
23
29
|
readonly exempt?: boolean | undefined;
|
|
24
30
|
readonly onDeadline?: OnDeadline | undefined;
|
|
31
|
+
// A regex the program's working directory must match too, for what its arguments cannot name.
|
|
32
|
+
readonly cwd?: string | undefined;
|
|
33
|
+
// The memory gate's price by name (memory-room.mjs SIZE_BYTES) instead of its class's; an unknown name is the class.
|
|
34
|
+
readonly size?: string | undefined;
|
|
35
|
+
// Judged even under a program already holding a slot, while every held pool is one no `nests` rule names.
|
|
36
|
+
readonly nests?: boolean | undefined;
|
|
25
37
|
readonly disabled?: boolean | undefined;
|
|
26
38
|
}
|
|
27
39
|
|
|
@@ -57,6 +69,14 @@ export interface HeavyMatch {
|
|
|
57
69
|
readonly limit: number;
|
|
58
70
|
readonly maxHold: number;
|
|
59
71
|
readonly onDeadline: OnDeadline;
|
|
72
|
+
readonly size?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Where a program runs, for the rules that look past its arguments.
|
|
76
|
+
export interface MatchWhere {
|
|
77
|
+
readonly cwd?: string | undefined;
|
|
78
|
+
// The pools programs above it already hold slots of (heldPoolsOf).
|
|
79
|
+
readonly heldPools?: readonly string[] | undefined;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
export const QUEUE_SKIPPED_EXIT_CODE: 75;
|
|
@@ -65,11 +85,13 @@ export const SHIPPED_HEAVY_COMMANDS: HeavyCommandSettings & { readonly rules: re
|
|
|
65
85
|
export const EARLIER_SHIPPED_RULES: readonly HeavyCommandRule[];
|
|
66
86
|
export function mergeHeavyRules(overrides?: HeavyCommandOverrides): HeavyCommands;
|
|
67
87
|
export function overridesOf(full: Partial<HeavyCommandSettings> & { readonly rules?: readonly HeavyCommandRule[] | undefined }): HeavyCommandOverrides;
|
|
68
|
-
export function matchInvocation(invocation: string, config: HeavyCommands, report?: (detail: string) => void): HeavyMatch | undefined;
|
|
88
|
+
export function matchInvocation(invocation: string, config: HeavyCommands, report?: (detail: string) => void, where?: MatchWhere): HeavyMatch | undefined;
|
|
69
89
|
export function queueArgs(match: HeavyMatch, config: HeavyCommandSettings): string[];
|
|
70
90
|
// Whether a test invocation names the files it runs, for the memory gate's smaller price.
|
|
71
91
|
export function targetedRun(invocation: string): boolean;
|
|
72
|
-
// queue-run's `--size targeted` for a targeted test run under a test rule, else nothing.
|
|
92
|
+
// queue-run's `--size <name>`: the rule's own size, else `targeted` for a targeted test run under a test rule, else nothing.
|
|
73
93
|
export function sizeArgs(match: HeavyMatch, invocation: string): string[];
|
|
74
94
|
export function holdWarnSeconds(maxHold: number): number | undefined;
|
|
75
95
|
export function ruleById(config: HeavyCommands, id: string): HeavyCommandRule | undefined;
|
|
96
|
+
// The pools the programs above one already hold slots of, from INTENTIC_HEAVY_HELD and INTENTIC_QUEUE_SLOT.
|
|
97
|
+
export function heldPoolsOf(env: Readonly<Record<string, string | undefined>>, config: HeavyCommands): string[];
|
package/src/heavy/heavy-turn.cjs
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// A heavy program is put in the toolchain class (its rank for the OOM killer, its CPU and IO priority) right away,
|
|
6
6
|
// whether or not anything queues it, then replaces itself with the same run behind queue-run (a slot, and memory first),
|
|
7
7
|
// or behind offload-run when the owner sends that kind of work to a runner. Everything it starts inherits both, and
|
|
8
|
-
// nothing below it is judged again
|
|
8
|
+
// nothing below it is judged again (INTENTIC_HEAVY_HELD and queue-run's INTENTIC_QUEUE_SLOT say it is already covered)
|
|
9
|
+
// except by a `nests` rule while every pool held above is one no `nests` rule names (heavy-rules.cjs judgedUnder): the
|
|
10
|
+
// editor's `vue-tsc` takes its own lane even under a `pnpm -r typecheck` that holds a heavy slot.
|
|
9
11
|
//
|
|
10
12
|
// The daemon hands the table in INTENTIC_HEAVY, as JSON: { rules (heavy-rules.cjs mergeHeavyRules), queue, queueRun,
|
|
11
13
|
// offloadRun, offload: { <rule id>: <runner> }, klass: { oomScoreAdj, nice, lowIo } }. Every failure here is a program
|
|
@@ -15,17 +17,18 @@
|
|
|
15
17
|
const { spawnSync } = require("node:child_process");
|
|
16
18
|
const { accessSync, constants, readFileSync, writeFileSync } = require("node:fs");
|
|
17
19
|
const { getPriority, setPriority } = require("node:os");
|
|
18
|
-
const { matchInvocation, queueArgs, sizeArgs } = require("./heavy-rules.cjs");
|
|
20
|
+
const { heldPoolsOf, matchInvocation, queueArgs, sizeArgs } = require("./heavy-rules.cjs");
|
|
19
21
|
|
|
20
|
-
// The spec the daemon handed down, or undefined when this program is not under it
|
|
21
|
-
const
|
|
22
|
+
// The spec the daemon handed down, or undefined when this program is not under it.
|
|
23
|
+
const handedSpec = (env) => {
|
|
22
24
|
const raw = env.INTENTIC_HEAVY;
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
return JSON.parse(raw);
|
|
25
|
+
return raw === undefined || raw === "" ? undefined : JSON.parse(raw);
|
|
27
26
|
};
|
|
28
27
|
|
|
28
|
+
// The spec the daemon handed down, or undefined when this program is not under it or is already covered.
|
|
29
|
+
const pendingSpec = (env = process.env) =>
|
|
30
|
+
env.INTENTIC_HEAVY_HELD !== undefined || env.INTENTIC_QUEUE_SLOT !== undefined ? undefined : handedSpec(env);
|
|
31
|
+
|
|
29
32
|
// The class applied to this very process, raised only: descendants inherit all three at fork.
|
|
30
33
|
const applyClass = (klass) => {
|
|
31
34
|
if (klass === undefined || process.platform !== "linux") {
|
|
@@ -87,13 +90,13 @@ const become = (argv, env) => {
|
|
|
87
90
|
* Judges one program as it starts. Returns false for one that is not heavy (or not under the daemon's table), true for
|
|
88
91
|
* one that was classed and runs in place; a queued or offloaded one does not return.
|
|
89
92
|
*/
|
|
90
|
-
const takeTurn = ({ program, args, command, env = process.env }) => {
|
|
91
|
-
const spec =
|
|
93
|
+
const takeTurn = ({ program, args, command, env = process.env, cwd = process.cwd() }) => {
|
|
94
|
+
const spec = handedSpec(env);
|
|
92
95
|
if (spec === undefined) {
|
|
93
96
|
return false;
|
|
94
97
|
}
|
|
95
98
|
const invocation = [program, ...args].join(" ");
|
|
96
|
-
const match = matchInvocation(invocation, spec.rules);
|
|
99
|
+
const match = matchInvocation(invocation, spec.rules, undefined, { cwd, heldPools: heldPoolsOf(env, spec.rules) });
|
|
97
100
|
if (match === undefined) {
|
|
98
101
|
return false;
|
|
99
102
|
}
|
package/src/index.ts
CHANGED
|
@@ -145,3 +145,9 @@ export const GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.a
|
|
|
145
145
|
|
|
146
146
|
// localStorage slot for the cached ID token, keyed by client id so a rotation can't reuse it.
|
|
147
147
|
export const GOOGLE_TOKEN_STORAGE_KEY = `intentic.gid.${GOOGLE_CLIENT_ID}`;
|
|
148
|
+
|
|
149
|
+
// The platform's sign-in plugin and the editor's code fields must accept the same number of digits.
|
|
150
|
+
export const SIGN_IN_CODE_LENGTH = 6;
|
|
151
|
+
|
|
152
|
+
// Sandbox registry and fleet requests, suggested names, and the editor's name fields share this bound.
|
|
153
|
+
export const SANDBOX_NAME_MAX = 60;
|
package/src/memory-room.d.mts
CHANGED
|
@@ -4,8 +4,11 @@ export type RoomWorkload = "agentRuntime" | "service" | "panel" | "install" | "c
|
|
|
4
4
|
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
|
-
// A toolchain command
|
|
8
|
-
|
|
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";
|
|
10
|
+
export const SIZE_BYTES: Readonly<Record<RoomSize, number>>;
|
|
11
|
+
export function isRoomSize(size: string): size is RoomSize;
|
|
9
12
|
export const TEST_PROCESS_BYTES: Readonly<{ fanOutWorker: number; standaloneWorker: number; typecheck: number; ceiling: number }>;
|
|
10
13
|
export const STALL_PERCENT: number;
|
|
11
14
|
export const STALL_SUSTAINED_PERCENT: number;
|
package/src/memory-room.mjs
CHANGED
|
@@ -30,6 +30,23 @@ export const COST_BYTES = Object.freeze({
|
|
|
30
30
|
// the invocation, and queue-run passes it as `--size targeted`.
|
|
31
31
|
export const TARGETED_RUN_BYTES = 512 * MIB;
|
|
32
32
|
|
|
33
|
+
// What a toolchain command a heavy-command rule prices by name takes, instead of its class's cost (heavy-rules.cjs
|
|
34
|
+
// `size`, which queue-run passes as `--size <name>`). One name per measured shape:
|
|
35
|
+
export const SIZE_BYTES = Object.freeze({
|
|
36
|
+
targeted: TARGETED_RUN_BYTES,
|
|
37
|
+
// The editor's full typecheck, `vue-tsc` over `_editor/web`, or `_site/demo`'s, which compiles the same source:
|
|
38
|
+
// 6.8 GiB at its peak (measured 2026-10-26, cold and warm alike), under the 8 GiB heap its script gives it.
|
|
39
|
+
"editor-typecheck": 7 * GIB,
|
|
40
|
+
// `lsp diag` on a Vue package: `vue-tsc` over the files a change touches and their direct importers
|
|
41
|
+
// (_search/lsp/src/vue-check.ts). Measured 2026-10-27 on `_editor/web`, no build info: 3.9 GiB at its peak for
|
|
42
|
+
// AgentsView.vue (1 changed file, 8 importers, about 1,800 files loaded), 2.0 GiB for ChatPermissionCard.vue; with
|
|
43
|
+
// the build info a run leaves, 2.4 and 1.0 GiB.
|
|
44
|
+
"scoped-typecheck": 4 * GIB,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Whether `size` is a price the gate knows; any other word is priced as its class. */
|
|
48
|
+
export const isRoomSize = (size) => Object.hasOwn(SIZE_BYTES, size);
|
|
49
|
+
|
|
33
50
|
// What one test or typecheck process holds at its peak, for the scripts that size a fan-out to the free memory
|
|
34
51
|
// (_tools/scripts/verify/test-workers.mjs) and the ceiling that stops a runaway one (_tools/scripts/lib/memory-ceiling.mjs).
|
|
35
52
|
export const TEST_PROCESS_BYTES = Object.freeze({
|
|
@@ -42,7 +59,7 @@ export const TEST_PROCESS_BYTES = Object.freeze({
|
|
|
42
59
|
// One typecheck task: most packages' tsgo or vue-tsc settle under 1 GiB, the web package's vue-tsc may take its 8 GiB
|
|
43
60
|
// heap (measured 2026-10-06: 4.8 GiB from no build info, 1 GiB of it vue-i18n's `t` checked against the catalog;
|
|
44
61
|
// 2026-10-20: 6.5 GiB, past the 6 GiB it had, which only a check seeded with build info stayed under), and at most
|
|
45
|
-
// one of those runs among the others.
|
|
62
|
+
// one of those runs among the others: the heavy-command table's `vue-typecheck` lane holds them to one.
|
|
46
63
|
typecheck: 2 * GIB,
|
|
47
64
|
// Past this one process is a leak, not a suite: twice the largest worker measured. On 2026-09-25 a single bun process
|
|
48
65
|
// reached 14.7 GiB resident plus 31 GiB of swap, twice in one day, and stalled every conversation on the sandbox.
|
|
@@ -174,9 +191,9 @@ export const readReadingSync = () => readingFrom(readSync);
|
|
|
174
191
|
|
|
175
192
|
const gib = (bytes) => `${(bytes / GIB).toFixed(1)} GiB`;
|
|
176
193
|
|
|
177
|
-
/** What one more of `workload` costs: its class's cost, or a
|
|
194
|
+
/** What one more of `workload` costs: its class's cost, or a named toolchain shape's where `size` names one. */
|
|
178
195
|
export const costBytesOf = (workload, size) =>
|
|
179
|
-
workload === "toolchain" && size
|
|
196
|
+
workload === "toolchain" && size !== undefined && isRoomSize(size) ? SIZE_BYTES[size] : (COST_BYTES[workload] ?? COST_BYTES.agentRuntime);
|
|
180
197
|
|
|
181
198
|
/** What starting one more of `workload` needs free: its own cost, plus a person's turn when nobody is waiting on it. */
|
|
182
199
|
export const needBytes = (workload, attended, size) => costBytesOf(workload, size) + (attended ? 0 : PERSON_RESERVE_BYTES);
|
|
@@ -338,7 +355,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
338
355
|
/**
|
|
339
356
|
* The daemon's verdict for one more of `workload`, waiting up to `waitSeconds` for room; where no daemon answers, the
|
|
340
357
|
* same formula over the same files, polled every `intervalMs`. Never throws: every failure is an answer of `run`.
|
|
341
|
-
* `size
|
|
358
|
+
* `size` prices a toolchain command by its named shape (SIZE_BYTES: `targeted` for one aimed at named files); `pid` is the process the
|
|
342
359
|
* admitted work runs as, whose exit hands its reservation back at once instead of after RESERVATION_MS.
|
|
343
360
|
*/
|
|
344
361
|
export const askRoom = async ({
|
|
@@ -364,7 +381,7 @@ export const askRoom = async ({
|
|
|
364
381
|
return { ...verdict, waitedMs: Date.now() - startedAt, source: "formula" };
|
|
365
382
|
};
|
|
366
383
|
|
|
367
|
-
// `memory-room --class toolchain --wait 120 --label NAME [--size targeted] [--pid PID]` holds a command until there is
|
|
384
|
+
// `memory-room --class toolchain --wait 120 --label NAME [--size targeted|editor-typecheck|…] [--pid PID]` holds a command until there is
|
|
368
385
|
// room, printing why it waited, and always exits 0: a gate that cannot read the sandbox must not be the reason a command
|
|
369
386
|
// did not run. `--json` prints the answer instead; `--free` prints the free memory as the daemon counts it, for a caller
|
|
370
387
|
// that sizes itself to it.
|
|
@@ -385,7 +402,7 @@ const main = async (args) => {
|
|
|
385
402
|
waitSeconds,
|
|
386
403
|
label,
|
|
387
404
|
intervalMs: Math.max(50, Number(option("interval-ms", "5000")) || 5000),
|
|
388
|
-
size: option("size",
|
|
405
|
+
size: isRoomSize(option("size", "")) ? option("size", "") : undefined,
|
|
389
406
|
pid: Number.isInteger(pid) && pid > 1 ? pid : undefined,
|
|
390
407
|
});
|
|
391
408
|
if (args.includes("--json")) {
|