grepmax 0.26.58 → 0.26.60

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.
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HOST_SAMPLE_TIMEOUT_MS = void 0;
37
+ exports.parseResourceProcesses = parseResourceProcesses;
38
+ exports.parseBatchFootprints = parseBatchFootprints;
39
+ exports.sampleHostResources = sampleHostResources;
40
+ const node_child_process_1 = require("node:child_process");
41
+ const os = __importStar(require("node:os"));
42
+ const path = __importStar(require("node:path"));
43
+ const kernel_zone_1 = require("./kernel-zone");
44
+ const process_footprint_1 = require("./process-footprint");
45
+ exports.HOST_SAMPLE_TIMEOUT_MS = 3000;
46
+ const MAX_PROCESSES = 64;
47
+ const PS_ARGS = ["-axo", "uid=,pid=,ppid=,pgid=,lstart=,comm="];
48
+ const ROLES = {
49
+ "gmax-daemon": "daemon",
50
+ "gmax-mcp": "mcp",
51
+ "gmax-worker": "worker",
52
+ "gmax-embed": "embedding",
53
+ gmax: "client",
54
+ };
55
+ /** No argv/environment inspection. Include all same-user gmax clients, then
56
+ * their entire child trees (uv launchers are not substitutes for Python). */
57
+ function parseResourceProcesses(output, uid, selfPid, extraPids = [], extraGroups = []) {
58
+ var _a;
59
+ const rows = output
60
+ .trim()
61
+ .split("\n")
62
+ .map((line) => {
63
+ const match = /^\s*(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+)$/.exec(line);
64
+ if (!match)
65
+ throw new Error("process inventory could not be parsed");
66
+ return {
67
+ uid: Number(match[1]),
68
+ pid: Number(match[2]),
69
+ parentPid: Number(match[3]),
70
+ groupPid: Number(match[4]),
71
+ start: match[5].replace(/\s+/g, " "),
72
+ command: path.basename(match[6].trim()),
73
+ };
74
+ })
75
+ // ps sees itself as our child. It exits before footprint runs and is not
76
+ // part of gmax's working set; including it makes every sample incomplete.
77
+ .filter((row) => row.uid === uid && !(row.parentPid === selfPid && row.command === "ps"));
78
+ const selected = new Map();
79
+ for (const row of rows) {
80
+ const role = (_a = ROLES[row.command]) !== null && _a !== void 0 ? _a : (row.pid === selfPid
81
+ ? "client"
82
+ : extraPids.includes(row.pid) || extraGroups.includes(row.groupPid)
83
+ ? "embedding"
84
+ : undefined);
85
+ if (role)
86
+ selected.set(row.pid, {
87
+ pid: row.pid,
88
+ parentPid: row.parentPid,
89
+ groupPid: row.groupPid,
90
+ start: row.start,
91
+ role,
92
+ footprintMb: null,
93
+ });
94
+ }
95
+ let added = true;
96
+ while (added) {
97
+ added = false;
98
+ for (const row of rows) {
99
+ if (selected.has(row.pid) || !selected.has(row.parentPid))
100
+ continue;
101
+ selected.set(row.pid, {
102
+ pid: row.pid,
103
+ parentPid: row.parentPid,
104
+ groupPid: row.groupPid,
105
+ start: row.start,
106
+ role: "helper",
107
+ footprintMb: null,
108
+ });
109
+ added = true;
110
+ }
111
+ }
112
+ if (!selected.has(selfPid))
113
+ throw new Error("required process missing from inventory");
114
+ if (selected.size > MAX_PROCESSES)
115
+ throw new Error("process inventory exceeds 64 processes");
116
+ return [...selected.values()].sort((a, b) => a.pid - b.pid);
117
+ }
118
+ function parseBatchFootprints(output) {
119
+ const result = new Map();
120
+ for (const line of output.split("\n")) {
121
+ const match = /\[(\d+)\]:.*\bFootprint:/.exec(line);
122
+ if (!match)
123
+ continue;
124
+ const value = (0, process_footprint_1.parseFootprintMb)(line);
125
+ const pid = Number(match[1]);
126
+ if (value === null || value < 0 || result.has(pid))
127
+ throw new Error("invalid process footprint");
128
+ result.set(pid, value);
129
+ }
130
+ return result;
131
+ }
132
+ function sampleHostResources(extraPids = [], overrides = {}, extraGroups = []) {
133
+ var _a;
134
+ var _b, _c;
135
+ const deps = Object.assign({ platform: process.platform, pid: process.pid, uid: (_b = (_a = process.getuid) === null || _a === void 0 ? void 0 : _a.call(process)) !== null && _b !== void 0 ? _b : -1, now: Date.now, monotonic: () => performance.now(), freeBytes: os.freemem, run: (command, args, timeoutMs) => (0, node_child_process_1.execFileSync)(command, args, {
136
+ encoding: "utf8",
137
+ timeout: timeoutMs,
138
+ maxBuffer: 512 * 1024,
139
+ stdio: ["ignore", "pipe", "ignore"],
140
+ }) }, overrides);
141
+ const snapshot = {
142
+ at: deps.now(),
143
+ completedAt: deps.now(),
144
+ platform: deps.platform,
145
+ processes: [],
146
+ aggregateFootprintMb: null,
147
+ physicalFreeMb: null,
148
+ swapUsedMb: null,
149
+ memoryPressure: deps.platform === "darwin" ? "unknown" : "unsupported",
150
+ kernelPressure: deps.platform === "darwin" ? "unknown" : "unsupported",
151
+ kernelBytes: null,
152
+ incompleteReasons: [],
153
+ };
154
+ if (deps.platform !== "darwin")
155
+ return snapshot;
156
+ const deadline = deps.monotonic() + exports.HOST_SAMPLE_TIMEOUT_MS;
157
+ const run = (command, args, maximumMs = 500) => {
158
+ const remaining = Math.floor(deadline - deps.monotonic());
159
+ if (remaining <= 0)
160
+ throw new Error("resource sample deadline exceeded");
161
+ return deps.run(command, args, Math.min(maximumMs, remaining));
162
+ };
163
+ try {
164
+ const bytes = deps.freeBytes();
165
+ if (Number.isFinite(bytes) && bytes >= 0)
166
+ snapshot.physicalFreeMb = bytes / 1048576;
167
+ }
168
+ catch (_d) { }
169
+ try {
170
+ const zone = (0, kernel_zone_1.parseZprintOutput)(run("zprint", ["-L", "data.kalloc.1024"]));
171
+ if (zone) {
172
+ const bytes = zone.elements * zone.elementSize;
173
+ if (Number.isSafeInteger(bytes)) {
174
+ snapshot.kernelBytes = bytes;
175
+ snapshot.kernelPressure = (0, kernel_zone_1.classifyZonePressure)(bytes);
176
+ }
177
+ }
178
+ }
179
+ catch (_e) { }
180
+ try {
181
+ const swap = /\bused\s*=\s*([\d.]+)([MG])/i.exec(run("sysctl", ["-n", "vm.swapusage"]));
182
+ if (swap) {
183
+ const mb = Number(swap[1]) * (swap[2].toUpperCase() === "G" ? 1024 : 1);
184
+ if (Number.isFinite(mb) && mb >= 0)
185
+ snapshot.swapUsedMb = mb;
186
+ }
187
+ }
188
+ catch (_f) { }
189
+ try {
190
+ snapshot.processes = parseResourceProcesses(run("ps", PS_ARGS), deps.uid, deps.pid, extraPids, extraGroups);
191
+ const footprints = parseBatchFootprints(run("footprint", [
192
+ "--noCategories",
193
+ ...snapshot.processes.flatMap((p) => ["-p", String(p.pid)]),
194
+ ], 1500));
195
+ for (const p of snapshot.processes)
196
+ p.footprintMb = (_c = footprints.get(p.pid)) !== null && _c !== void 0 ? _c : null;
197
+ const after = parseResourceProcesses(run("ps", PS_ARGS), deps.uid, deps.pid, extraPids, extraGroups);
198
+ const identity = (p) => [
199
+ p.pid,
200
+ p.parentPid,
201
+ p.groupPid,
202
+ p.start,
203
+ p.role,
204
+ ];
205
+ if (JSON.stringify(after.map(identity)) !==
206
+ JSON.stringify(snapshot.processes.map(identity)))
207
+ throw new Error("process inventory changed during sample");
208
+ if (snapshot.processes.some((p) => p.footprintMb === null))
209
+ throw new Error("process footprint unavailable");
210
+ snapshot.aggregateFootprintMb = snapshot.processes.reduce((sum, p) => sum + p.footprintMb, 0);
211
+ }
212
+ catch (error) {
213
+ snapshot.incompleteReasons.push(error instanceof Error &&
214
+ /^(process|required|resource)/.test(error.message)
215
+ ? error.message
216
+ : "process inventory or footprint unavailable");
217
+ snapshot.aggregateFootprintMb = null;
218
+ }
219
+ // Fresh cheap pressure check after the batch; an earlier normal value cannot
220
+ // authorize a fork after pressure changes during native sampling.
221
+ try {
222
+ const raw = run("sysctl", [
223
+ "-n",
224
+ "kern.memorystatus_vm_pressure_level",
225
+ ]).trim();
226
+ snapshot.memoryPressure =
227
+ raw === "1"
228
+ ? "normal"
229
+ : raw === "2"
230
+ ? "warn"
231
+ : raw === "4"
232
+ ? "critical"
233
+ : "unknown";
234
+ }
235
+ catch (_g) { }
236
+ if (snapshot.memoryPressure === "unknown")
237
+ snapshot.incompleteReasons.push("OS memory pressure unavailable");
238
+ if (snapshot.kernelPressure === "unknown")
239
+ snapshot.incompleteReasons.push("kernel pressure unavailable");
240
+ if (snapshot.physicalFreeMb === null)
241
+ snapshot.incompleteReasons.push("physical free pages unavailable");
242
+ if (snapshot.swapUsedMb === null)
243
+ snapshot.incompleteReasons.push("swap usage unavailable");
244
+ snapshot.completedAt = deps.now();
245
+ if (deps.monotonic() > deadline)
246
+ snapshot.incompleteReasons.push("resource sample deadline exceeded");
247
+ return snapshot;
248
+ }
@@ -0,0 +1,409 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.resourceBudget = exports.ResourceBudget = exports.ResourceAdmissionError = exports.HELPER_RESOURCE_RESERVE_MB = exports.EMBEDDING_RESOURCE_RESERVE_MB = exports.WORKER_RESOURCE_RESERVE_MB = exports.DEFAULT_RESOURCE_BUDGET_MB = void 0;
40
+ exports.resolveResourceBudgetMb = resolveResourceBudgetMb;
41
+ exports.assertResourceSnapshot = assertResourceSnapshot;
42
+ const node_child_process_1 = require("node:child_process");
43
+ const node_crypto_1 = require("node:crypto");
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
47
+ const config_1 = require("../../config");
48
+ const autostart_1 = require("./autostart");
49
+ const host_resource_1 = require("./host-resource");
50
+ const safety_latch_1 = require("./safety-latch");
51
+ /** Admission thresholds, not an OS-enforced memory cap. Keep enough room for
52
+ * a worker's measured 1536MiB recycle threshold, and charge pending launches
53
+ * before another session can admit work. Actual footprint can exceed a reserve;
54
+ * subsequent admission/heartbeat then pauses heavy work. */
55
+ exports.DEFAULT_RESOURCE_BUDGET_MB = 6144;
56
+ exports.WORKER_RESOURCE_RESERVE_MB = 1536;
57
+ exports.EMBEDDING_RESOURCE_RESERVE_MB = 1024;
58
+ exports.HELPER_RESOURCE_RESERVE_MB = 512;
59
+ class ResourceAdmissionError extends Error {
60
+ constructor(message, critical = false) {
61
+ super(message);
62
+ this.critical = critical;
63
+ this.code = "HOST_RESOURCE";
64
+ this.name = "ResourceAdmissionError";
65
+ }
66
+ }
67
+ exports.ResourceAdmissionError = ResourceAdmissionError;
68
+ function resolveResourceBudgetMb(raw = process.env.GMAX_RESOURCE_BUDGET_MB) {
69
+ if (raw === undefined)
70
+ return exports.DEFAULT_RESOURCE_BUDGET_MB;
71
+ const n = Number(raw);
72
+ // A malformed/disabled budget must not silently become unlimited.
73
+ if (!Number.isInteger(n) || n < 256 || n > exports.DEFAULT_RESOURCE_BUDGET_MB)
74
+ throw new ResourceAdmissionError("resource budget must be 256..6144 MiB");
75
+ return n;
76
+ }
77
+ function assertResourceSnapshot(snapshot, chargedMb, budgetMb, now = Date.now()) {
78
+ if (snapshot.memoryPressure === "critical" ||
79
+ snapshot.kernelPressure === "critical")
80
+ throw new ResourceAdmissionError("critical host pressure; heavy work refused", true);
81
+ if (snapshot.memoryPressure === "warn" || snapshot.kernelPressure === "warn")
82
+ throw new ResourceAdmissionError("warning host pressure; heavy work paused");
83
+ if (snapshot.platform !== "darwin")
84
+ return;
85
+ if (now < snapshot.at ||
86
+ now - snapshot.at > 5000 ||
87
+ snapshot.completedAt < snapshot.at ||
88
+ snapshot.completedAt > now)
89
+ throw new ResourceAdmissionError("resource sample stale or clock changed; heavy work paused");
90
+ if (snapshot.incompleteReasons.length ||
91
+ snapshot.aggregateFootprintMb === null ||
92
+ snapshot.memoryPressure !== "normal" ||
93
+ snapshot.kernelPressure !== "ok")
94
+ throw new ResourceAdmissionError(`unknown aggregate resources; heavy work paused: ${snapshot.incompleteReasons.join(", ") || "incomplete sample"}`);
95
+ if (!Number.isFinite(chargedMb) ||
96
+ chargedMb < 0 ||
97
+ !Number.isFinite(budgetMb) ||
98
+ budgetMb <= 0 ||
99
+ chargedMb > budgetMb)
100
+ throw new ResourceAdmissionError(`aggregate resource budget exceeded (${Math.ceil(chargedMb)}/${budgetMb} MiB); heavy work paused`);
101
+ }
102
+ /** One ledger under the shared home, independent of which store a client uses.
103
+ * Reservations survive parent failure and installation; only a verified dead
104
+ * or reused PID may be reaped. Unknown/corrupt owners refuse expansion. */
105
+ class ResourceBudget {
106
+ constructor(overrides = {}) {
107
+ this.deps = Object.assign({ root: path.join(config_1.PATHS.sharedRoot, "resource-budget"), platform: process.platform, pid: process.pid, now: Date.now, sample: (extraPids = []) => {
108
+ const records = this.readRecords();
109
+ return (0, host_resource_1.sampleHostResources)([...records.map((r) => r.pid), ...extraPids], {}, records.flatMap((r) => (r.groupPid ? [r.groupPid] : [])));
110
+ }, signal: (pid) => process.kill(pid, 0), quarantine: autostart_1.daemonStartDeniedReason, latch: safety_latch_1.latchSafetyStop, processStart: () => (0, node_child_process_1.execFileSync)("ps", ["-p", String(process.pid), "-o", "lstart="], {
111
+ encoding: "utf8",
112
+ timeout: 500,
113
+ maxBuffer: 1024,
114
+ stdio: ["ignore", "pipe", "ignore"],
115
+ })
116
+ .trim()
117
+ .replace(/\s+/g, " ") }, overrides);
118
+ }
119
+ withLock(fn) {
120
+ const { root } = this.deps;
121
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
122
+ const stat = fs.lstatSync(root);
123
+ if (!stat.isDirectory() || stat.isSymbolicLink())
124
+ throw new ResourceAdmissionError("unverified resource budget directory");
125
+ fs.chmodSync(root, 0o700);
126
+ const file = path.join(root, "admission");
127
+ const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_NOFOLLOW | fs.constants.O_WRONLY, 0o600);
128
+ fs.closeSync(fd);
129
+ let release;
130
+ try {
131
+ // Synchronous sections contain only a <=3s sample and bounded metadata.
132
+ // Competing sessions refuse immediately instead of blocking their tools.
133
+ release = proper_lockfile_1.default.lockSync(file, {
134
+ retries: 0,
135
+ stale: 30000,
136
+ realpath: false,
137
+ });
138
+ }
139
+ catch (_a) {
140
+ throw new ResourceAdmissionError("resource admission busy or unavailable; retry later");
141
+ }
142
+ try {
143
+ return fn();
144
+ }
145
+ finally {
146
+ release();
147
+ }
148
+ }
149
+ readRecords() {
150
+ const entries = fs.readdirSync(this.deps.root);
151
+ if (entries.some((name) => name.endsWith(".tmp")))
152
+ throw new ResourceAdmissionError("interrupted resource reservation; heavy work paused");
153
+ const files = entries.filter((name) => name.endsWith(".json"));
154
+ if (files.length > 128)
155
+ throw new ResourceAdmissionError("resource reservation limit exceeded");
156
+ const records = [];
157
+ for (const name of files) {
158
+ const file = path.join(this.deps.root, name);
159
+ let record;
160
+ try {
161
+ const stat = fs.lstatSync(file);
162
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
163
+ throw Error();
164
+ record = JSON.parse(fs.readFileSync(file, "utf8"));
165
+ if (record.version !== 1 ||
166
+ !Number.isSafeInteger(record.pid) ||
167
+ record.pid < 1 ||
168
+ typeof record.start !== "string" ||
169
+ !record.start ||
170
+ typeof record.nonce !== "string" ||
171
+ name !== `${record.nonce}.json` ||
172
+ !Number.isFinite(record.mb) ||
173
+ record.mb <= 0 ||
174
+ record.mb > exports.DEFAULT_RESOURCE_BUDGET_MB ||
175
+ (record.unverified !== undefined &&
176
+ typeof record.unverified !== "boolean") ||
177
+ typeof record.kind !== "string")
178
+ throw Error();
179
+ }
180
+ catch (_a) {
181
+ throw new ResourceAdmissionError("unverified resource reservation; heavy work paused");
182
+ }
183
+ if (record.groupPid !== undefined && record.groupPid !== record.pid)
184
+ throw new ResourceAdmissionError("unverified resource process group");
185
+ records.push(record);
186
+ }
187
+ return records;
188
+ }
189
+ records(snapshot) {
190
+ const records = [];
191
+ for (const record of this.readRecords()) {
192
+ const file = path.join(this.deps.root, `${record.nonce}.json`);
193
+ const live = snapshot.processes.find((p) => p.pid === record.pid);
194
+ if (record.unverified) {
195
+ try {
196
+ this.deps.signal(record.groupPid ? -record.groupPid : record.pid);
197
+ }
198
+ catch (error) {
199
+ if (error.code === "ESRCH") {
200
+ fs.unlinkSync(file);
201
+ continue;
202
+ }
203
+ }
204
+ throw new ResourceAdmissionError("unverified launched process; heavy work paused");
205
+ }
206
+ if (live && live.start !== record.start) {
207
+ fs.unlinkSync(file);
208
+ continue;
209
+ }
210
+ if (!live) {
211
+ try {
212
+ this.deps.signal(record.groupPid ? -record.groupPid : record.pid);
213
+ }
214
+ catch (error) {
215
+ if (error.code === "ESRCH") {
216
+ fs.unlinkSync(file);
217
+ continue;
218
+ }
219
+ }
220
+ throw new ResourceAdmissionError("resource reservation owner missing or unknown; heavy work paused");
221
+ }
222
+ records.push(record);
223
+ }
224
+ return records;
225
+ }
226
+ charge(snapshot, records) {
227
+ return snapshot.processes.reduce((sum, p) => { var _a; return sum +
228
+ Math.max((_a = p.footprintMb) !== null && _a !== void 0 ? _a : Number.NaN, records.filter((r) => r.pid === p.pid).reduce((n, r) => n + r.mb, 0)); }, 0);
229
+ }
230
+ persist(record) {
231
+ const file = path.join(this.deps.root, `${record.nonce}.json`);
232
+ const temporary = `${file}.tmp`;
233
+ const fd = fs.openSync(temporary, "wx", 0o600);
234
+ try {
235
+ fs.writeFileSync(fd, JSON.stringify(record));
236
+ fs.fsyncSync(fd);
237
+ }
238
+ finally {
239
+ fs.closeSync(fd);
240
+ }
241
+ fs.renameSync(temporary, file);
242
+ const dir = fs.openSync(this.deps.root, "r");
243
+ try {
244
+ fs.fsyncSync(dir);
245
+ }
246
+ finally {
247
+ fs.closeSync(dir);
248
+ }
249
+ }
250
+ checked(snapshot, records) {
251
+ try {
252
+ assertResourceSnapshot(snapshot, this.charge(snapshot, records), resolveResourceBudgetMb(), this.deps.now());
253
+ const legacy = snapshot.processes.filter((p) => p.role === "mcp" &&
254
+ !records.some((r) => r.pid === p.pid && r.kind === "client"));
255
+ if (legacy.length)
256
+ throw new ResourceAdmissionError(`${legacy.length} MCP client(s) need to reconnect after upgrading; heavy work paused`);
257
+ }
258
+ catch (error) {
259
+ if (error instanceof ResourceAdmissionError && error.critical) {
260
+ try {
261
+ this.deps.latch(error.message);
262
+ }
263
+ catch (_a) {
264
+ error.message += "; safety stop persistence failed";
265
+ }
266
+ }
267
+ throw error;
268
+ }
269
+ const denied = this.deps.quarantine();
270
+ if (denied)
271
+ throw new ResourceAdmissionError(`heavy work refused: ${denied}`);
272
+ }
273
+ /** Registration is metadata-only and must preserve STDIO availability under
274
+ * warning/unknown pressure. It proves this client participates in reservation
275
+ * admission; old clients cannot silently bypass the cross-session budget. */
276
+ registerClient() {
277
+ if (this.deps.platform !== "darwin")
278
+ return { attach: () => { }, release: () => { } };
279
+ const start = this.deps.processStart();
280
+ if (!start)
281
+ throw new ResourceAdmissionError("MCP process identity unavailable");
282
+ const record = {
283
+ version: 1,
284
+ pid: this.deps.pid,
285
+ start,
286
+ nonce: (0, node_crypto_1.randomUUID)(),
287
+ mb: 128,
288
+ kind: "client",
289
+ };
290
+ this.withLock(() => {
291
+ if (this.readRecords().length >= 128)
292
+ throw new ResourceAdmissionError("resource reservation limit exceeded");
293
+ this.persist(record);
294
+ });
295
+ return {
296
+ attach: () => {
297
+ throw new ResourceAdmissionError("client registration cannot transfer");
298
+ },
299
+ release: () => {
300
+ try {
301
+ fs.unlinkSync(path.join(this.deps.root, `${record.nonce}.json`));
302
+ }
303
+ catch (error) {
304
+ if (error.code !== "ENOENT")
305
+ throw error;
306
+ }
307
+ },
308
+ };
309
+ }
310
+ check(requiredPid) {
311
+ if (this.deps.platform !== "darwin")
312
+ return null;
313
+ if (requiredPid !== undefined &&
314
+ (!Number.isSafeInteger(requiredPid) ||
315
+ requiredPid === null ||
316
+ requiredPid < 1))
317
+ throw new ResourceAdmissionError("adopted model process identity unavailable; heavy work paused");
318
+ return this.withLock(() => {
319
+ const snapshot = this.deps.sample(requiredPid ? [requiredPid] : []);
320
+ if (requiredPid && !snapshot.processes.some((p) => p.pid === requiredPid))
321
+ throw new ResourceAdmissionError("adopted model process missing; heavy work paused");
322
+ this.checked(snapshot, this.records(snapshot));
323
+ return snapshot;
324
+ });
325
+ }
326
+ reserve(mb, kind) {
327
+ if (!Number.isFinite(mb) || mb <= 0 || mb > exports.DEFAULT_RESOURCE_BUDGET_MB)
328
+ throw new ResourceAdmissionError("invalid resource reservation");
329
+ if (this.deps.platform !== "darwin")
330
+ return { attach: () => { }, release: () => { } };
331
+ let record = this.withLock(() => {
332
+ var _a;
333
+ const denied = this.deps.quarantine();
334
+ if (denied)
335
+ throw new ResourceAdmissionError(`heavy work refused: ${denied}`);
336
+ const snapshot = this.deps.sample();
337
+ const owner = snapshot.processes.find((p) => p.pid === this.deps.pid);
338
+ if (!owner)
339
+ throw new ResourceAdmissionError("resource owner identity unavailable");
340
+ const records = this.records(snapshot);
341
+ const reserved = records
342
+ .filter((r) => r.pid === owner.pid)
343
+ .reduce((sum, r) => sum + r.mb, 0);
344
+ // Preserve the current working set as well as each new allocation. A
345
+ // large parent must not make a pending child appear free.
346
+ const charge = mb + Math.max(0, ((_a = owner.footprintMb) !== null && _a !== void 0 ? _a : Number.NaN) - reserved);
347
+ const next = {
348
+ version: 1,
349
+ pid: owner.pid,
350
+ start: owner.start,
351
+ nonce: (0, node_crypto_1.randomUUID)(),
352
+ mb: charge,
353
+ kind,
354
+ };
355
+ this.checked(snapshot, [...records, next]);
356
+ this.persist(next);
357
+ return next;
358
+ });
359
+ let released = false;
360
+ return {
361
+ attach: (pid, processGroup = false) => {
362
+ if (released)
363
+ throw new ResourceAdmissionError("resource reservation released");
364
+ if (!Number.isSafeInteger(pid) || pid < 1)
365
+ throw new ResourceAdmissionError("resource child PID unavailable");
366
+ // Publish uncertainty before probing/locking. This is our own nonce;
367
+ // moving its existing pending charge cannot grant any new capacity.
368
+ // A failed lock or killed parent must still account for the child.
369
+ record = Object.assign(Object.assign(Object.assign({}, record), { pid, start: "unverified", mb, unverified: true }), (processGroup ? { groupPid: pid } : {}));
370
+ this.persist(record);
371
+ this.withLock(() => {
372
+ const snapshot = this.deps.sample();
373
+ const child = snapshot.processes.find((p) => p.pid === pid);
374
+ if (!child ||
375
+ child.parentPid !== this.deps.pid ||
376
+ (processGroup && child.groupPid !== child.pid)) {
377
+ // The child was forked, even if the identity probe failed. Retain
378
+ // a durable uncertain charge across parent failure until it is
379
+ // confirmed dead; do not revert to a parent-only reservation.
380
+ record = Object.assign(Object.assign(Object.assign({}, record), { pid, start: "unverified", mb, unverified: true }), (processGroup ? { groupPid: pid } : {}));
381
+ this.persist(record);
382
+ throw new ResourceAdmissionError("resource child identity unavailable");
383
+ }
384
+ // Transfer does not admit more work. Preserve the charge even if
385
+ // pressure changed; the caller owns termination of its new child.
386
+ record = Object.assign(Object.assign(Object.assign({}, record), { pid: child.pid, start: child.start, mb, unverified: false }), (processGroup ? { groupPid: child.pid } : {}));
387
+ this.persist(record);
388
+ });
389
+ },
390
+ release: () => {
391
+ if (released)
392
+ return;
393
+ // Only the holder removes its nonce, after actual native/process close.
394
+ // Concurrent admission can overcharge or refuse a disappearing file,
395
+ // but removal never grants capacity before resources have been freed.
396
+ try {
397
+ fs.unlinkSync(path.join(this.deps.root, `${record.nonce}.json`));
398
+ }
399
+ catch (error) {
400
+ if (error.code !== "ENOENT")
401
+ throw error;
402
+ }
403
+ released = true;
404
+ },
405
+ };
406
+ }
407
+ }
408
+ exports.ResourceBudget = ResourceBudget;
409
+ exports.resourceBudget = new ResourceBudget();
@@ -3,18 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildResourceSnapshot = buildResourceSnapshot;
4
4
  function buildResourceSnapshot(counters, memory = process.memoryUsage(), at = Date.now()) {
5
5
  const mb = (bytes) => Math.round(bytes / 1024 / 1024);
6
- return {
7
- at,
8
- reason: counters.reason,
9
- footprintMb: counters.footprintMb,
10
- rssMb: mb(memory.rss),
11
- heapUsedMb: mb(memory.heapUsed),
12
- externalMb: mb(memory.external),
13
- arrayBuffersMb: mb(memory.arrayBuffers),
14
- lanceCacheMb: counters.lanceCacheBytes === null ? null : mb(counters.lanceCacheBytes),
15
- workers: counters.workers,
16
- pendingFiles: counters.pendingFiles,
17
- operations: counters.operations,
18
- maintenance: counters.maintenance,
19
- };
6
+ return Object.assign({ at, reason: counters.reason, footprintMb: counters.footprintMb, rssMb: mb(memory.rss), heapUsedMb: mb(memory.heapUsed), externalMb: mb(memory.external), arrayBuffersMb: mb(memory.arrayBuffers), lanceCacheMb: counters.lanceCacheBytes === null ? null : mb(counters.lanceCacheBytes), workers: counters.workers, pendingFiles: counters.pendingFiles, operations: counters.operations, maintenance: counters.maintenance }, (counters.host ? { host: counters.host } : {}));
20
7
  }