@ai-ecoverse/slicc-kernel 1.122.3 → 1.123.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -918,13 +918,22 @@ Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `
918
918
 
919
919
  The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
920
920
 
921
+ ### How many processes at once
922
+
923
+ In a browser every process worker of the page shares one renderer. V8 reserves about 8 GiB of address space for each 32-bit wasm memory, whatever its declared maximum, and its sandbox holds about 1 TiB. So a page runs about 125 wasm processes (or programs that own a wasm memory) at once, counting everything in the renderer (#970). Node has no such ceiling, and neither does Linux. The kernel keeps processes inside that ceiling:
924
+ - **The ceiling:** 112 by default in a browser and none in Node. It's the kernel's count of live wasm processes, with the kernel's other wasm memories in the page left outside it. When an instantiation runs out of memory anyway, the kernel learns the real ceiling at that moment (the live count minus one), and that learned value wins over the default.
925
+ - **Admission:** a spawn, fork, exec or `kernel.run` at the ceiling waits for another process to exit. It keeps waiting as long as processes exit or are admitted, and fails with `EAGAIN` only after 10 s with neither. A long `make -j` or `xargs -P` therefore queues, and a pipeline that needs more live processes than the ceiling fails cleanly. bash's own retry on a fork's `EAGAIN` (`fork: retry: Resource temporarily unavailable`) handles the rest.
926
+ - **Close to the ceiling** (within 16 of it), the kernel waits for each new process to finish instantiating before the spawn returns. If it ran out of memory, the kernel starts it again, quietly, once there's room, so the caller sees no failure.
927
+ - **The rare residual case:** a process that fails to instantiate with nobody waiting on it, which happens only when something outside the count fills the renderer. It ends with status 126 and `<program>: cannot allocate memory for a new process (too many processes at once)` on its stderr, never a JS `RangeError`.
928
+ - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling>` (the ceiling 0 for none).
929
+
921
930
  ### Metadata
922
931
 
923
932
  OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
924
933
 
925
934
  - mode bits (`chmod`; a `chmod` to the mode a file or directory already has writes nothing and leaves its ctime as it was, which saves a sidecar write for every installer that sets the mode a file was made with);
926
935
  - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
927
- - inode numbers that stay stable across renames;
936
+ - inode numbers that stay stable across renames. A file with no record has an inode derived from its path, and a rename keeps it on the file; the rename leaves a small record at the name it vacated with a fresh inode, so a file, symlink or directory made there later doesn't share the renamed one's ([#954](https://github.com/ai-ecoverse/slicc-kernel/issues/954)). An exclusive create doesn't count that record as a taken name, and the boot reconcile keeps it only while a live record still carries the inode it guards;
928
937
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
929
938
 
930
939
  OPFS stays authoritative: an entry is consulted only for a path that exists in OPFS (symlinks only for paths that don't), so files written straight to OPFS, for example by the BIOS installer or a page, get the defaults: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. A file deleted through the OPFS API disappears at once; its leftover entry is ignored and removed when the next kernel starts. Changes go to OPFS first and then to the sidecar, so a crash in between at worst loses the metadata of that one change. Each change is a read-modify-write inside one IndexedDB transaction, so two kernels (two tabs) on the same origin don't lose each other's updates.
@@ -1022,6 +1031,7 @@ The tools in `tools/cert-review/`:
1022
1031
  `.github/workflows/cert-review-pr.yml` certifies a pull request before it merges, so `main`, and with it npm's `latest`, only gets certified changes. It reports as the `cert-review` commit status, a required check of `main`'s merge queue. Every same-repo pull request gets one (`not behaviour`, `waits for merge-ok`, the review's verdict, or the self-guard's hold). **A pull request from a fork gets none**, because fork code never reaches the pools or the key, so it can't enter the queue: only Lars merges one, after reading it.
1023
1032
 
1024
1033
  - **Which pull requests:** those that change shipped behaviour, by a conservative path filter (`behaviourFiles` in `lib.mjs`): `src/`, `build.mjs`, `package.json` and its lock, `tsconfig*.json`, `.releaserc*`, `tools/release-recover.mjs`, the workflows that build or publish (`release.yml`, `ci.yml`), and the reviewer's own code (`tools/cert-review/`, `cert-review*.yml`, `.github/runner.mjs`). **Main's code decides:** every job checks out the pull request's base and fetches only the trees under test, so the path filter, pack, suite, findings, the instructions and record all run from `main`. The merge group's lookup loads its tools from the protected branch itself (`merge_group.base_ref`), not from the group's `base_sha`, which can be the speculative commit of the group ahead and carry the reviewer changes of the pull requests queued before it; `base_sha` is only the lookup's input. A pull request that changes the reviewer is reviewed by `main`'s reviewer. The workflow file of a `pull_request` run still comes from the pull request, so a change to the reviewer is held by the **certification self-guard**, the shared composite action `slicc-shared-web/.github/actions/cert-self-guard` at a pinned sha. Its light `self-guard` job runs on every pull request event and every merge group. When the diff touches the reviewer (`.github/workflows/cert*.yml`, `tools/cert-review/**`, `.github/runner.mjs` and the guard's own paths), `cert-review` is red with `reviewer change: needs coordinator review (cert-review-approved on this head)` and no review runs. The label is the Kernel Coordinator's alone: they add `cert-review-approved` after reading the change, and the review then starts with `merge-ok`. The approval is bound to the head's arrival on GitHub, and a push removes the label. A merge group that holds an unapproved reviewer change is red too. If the guard doesn't finish, the status is an error that asks to re-run the failed jobs. **The residual:** the pull request's YAML still decides what runs, so the guard catches an accidental edit, not a deliberate rewrite of this workflow. The pull request that adds this workflow can't pass its own check, because its base has no tools yet. Any other pull request gets a green status that says `not behaviour: <files>`, so the skip is recorded.
1034
+ - **A gate's silence never reads as a pass:** every tool a workflow gates on (`pr.mjs plan`, `same` and `lookup`, `same.mjs`, `pack.mjs`, `delta.mjs`, `suite.mjs`, `compat.mjs`, `findings.mjs`, `record.mjs`, `verify.mjs`) ends with one verdict line, `<tool>: PASS <what>` or `<tool>: FAIL <why>`, from the shared `verdict()` in `lib.mjs`. `tools/cert-review/gate.sh [--either|--evidence] <tool> <command>` runs a tool, passes its output through, and exits 70 with an error when the tool's last line starting `<tool>: ` isn't a PASS or FAIL verdict, so a tool replaced by `true`, an early exit 0 or an empty run fails its step. By default the step passes only on PASS with exit 0; a PASS with a non-zero exit, or a FAIL with exit 0, fails it. `--either` keeps the tool's own exit, for tools whose FAIL is evidence that record judges later (suite, compat). `--evidence` needs only the verdict, for the NEGATIVE run on main's tree, which is expected to fail. `suite: PASS` means exit 0, no failures and every expected file has results; `compat: PASS` means a run turned up with no regressions. `prompt.mjs` writes the review prompt to stdout, which goes into a step output, so it prints no verdict; a missing prompt leaves the review without `verdict.json`, and record marks that INCOMPLETE. Because the workflow of a pull request runs main's tools, the tools gain their verdicts first, and the workflows call them through `gate.sh` in the pull request after that.
1025
1035
  - **Decide by what ships:** when a pull request's behaviour files are only `package.json` and/or `package-lock.json` (a Renovate devDependency bump, say), both sides are packed as usual, and then the `same dist` job (`same.mjs`, in code) compares the two tarballs before anything else runs. It checks the same paths, entry types (a symlink isn't a file) and modes. `package.json` must be equal once `devDependencies`, `version` and `gitHead` are removed, so `scripts` (they ship, a consumer's install runs `postinstall`, and `prepack`/`prepublishOnly` can make the published build differ from the packed one), runtime dependencies, `exports`, `bin`, `files` and `engines` must all match. Every other file must be byte-identical once each side's **exact** version string (build.mjs bakes it into `dist`) is replaced by one placeholder. A version string in one more place, or any looser match, is a difference. Source maps are compared as JSON without their `mappings`, whose columns the longer or shorter version string shifts; the code itself is compared byte for byte. If all of that matches, `cert-review` is green with `not behaviour (identical dist): <candidate sha256> = <base sha256> after normalising <v1>/<v2>`, and the suites, kernel-compat, the review and the record are skipped. `same.json` (uploaded as an artifact) names the rule and both tarballs' full sha256. Any difference, or a `same dist` job that doesn't finish, means the full review. The kernel has no runtime `dependencies`, since esbuild bundles everything into `dist`, so a devDependency that changes the product shows up as a byte difference. A test-only devDependency is checked by `ci / test`, on the pull request and on the merge group.
1026
1036
  - **When:** only for a same-repo pull request with `merge-ok`, when the label is added and on every push while it is set. Each pull request has its own concurrency group, and a newer push cancels the older review. A behaviour change without `merge-ok` gets a red status, `waits for merge-ok to be reviewed`. Other labels don't start or cancel anything. Fork code never reaches the pools or the key.
1027
1037
  - **What:** the checkout is the event's default `refs/pull/N/merge`, the pull request merged onto `main` at review time, so a review is of what would land, without pushing to the branch. Both sides are packed in the same run, on a GitHub-hosted runner, with no secrets: the candidate (the merge ref, `x.y.z-pr.N.<run>`) and the base (its first parent, `main`). Then:
@@ -107,7 +107,7 @@ var MemoryMeta = class {
107
107
  if (entry) this.entries.set(path, entry);
108
108
  else this.entries.delete(path);
109
109
  }
110
- async move(from, to, root) {
110
+ async move(from, to, root, left) {
111
111
  await this.remove([to]);
112
112
  const carried = this.entries.subtree(from).map((path) => this.entries.get(path));
113
113
  for (const entry of carried) this.entries.delete(entry.path);
@@ -116,6 +116,7 @@ var MemoryMeta = class {
116
116
  if (path !== to) this.entries.set(path, moved(entry, path));
117
117
  }
118
118
  this.entries.set(to, moved(root(carried.find((entry) => entry.path === from)), to));
119
+ if (left) this.entries.set(from, left);
119
120
  }
120
121
  async remove(paths) {
121
122
  for (const path of paths) this.entries.deleteTree(path);
@@ -240,7 +241,7 @@ var IndexedDbMeta = class {
240
241
  this.channel.close();
241
242
  this.db.close();
242
243
  }
243
- async move(from, to, root) {
244
+ async move(from, to, root, left) {
244
245
  this.settleRemoves();
245
246
  const [transaction, store] = this.transaction("readwrite");
246
247
  store.delete(to);
@@ -254,6 +255,7 @@ var IndexedDbMeta = class {
254
255
  store.put(moved(entry, to + entry.path.slice(from.length)));
255
256
  }
256
257
  store.put(moved(root(source.result), to));
258
+ if (left) store.put(left);
257
259
  };
258
260
  await settle(transaction, () => void 0);
259
261
  this.changed({ trees: [from, to] });
@@ -1104,6 +1106,14 @@ function sidecar(meta) {
1104
1106
  });
1105
1107
  return ops;
1106
1108
  }
1109
+ function occupied(entry) {
1110
+ if (!entry?.vacated) return entry ?? {};
1111
+ const { vacated: _, ...kept } = entry;
1112
+ return kept;
1113
+ }
1114
+ function vacatedAt(path) {
1115
+ return { path, ino: inodeOf(`${path}\0${crypto.randomUUID()}`), vacated: true };
1116
+ }
1107
1117
  function missing2(err) {
1108
1118
  const code = err?.code;
1109
1119
  return code === "ENOENT" || code === "ENOTDIR";
@@ -1338,7 +1348,7 @@ var OpfsFs = class {
1338
1348
  const kind = directory ? void 0 : owner?.socket ? "socket" : owner?.fifo ? "fifo" : void 0;
1339
1349
  const unique = kind === "fifo" ? { ino: inodeOf(`${path}\0${crypto.randomUUID()}`) } : {};
1340
1350
  const fields = kind ? { ...owned4, ...unique, [kind]: true } : owned4;
1341
- if (fields) await this.meta.update(path, (entry) => ({ ...entry, path, ...fields }));
1351
+ if (fields) await this.meta.update(path, (entry) => ({ ...occupied(entry), path, ...fields }));
1342
1352
  }
1343
1353
  guarded = [];
1344
1354
  protect(prefixes2, versions = false) {
@@ -2050,8 +2060,9 @@ var OpfsFs = class {
2050
2060
  await this.locked(physical, async () => {
2051
2061
  if (await this.handleExists(dir, name2)) throw fsError("EEXIST", path);
2052
2062
  const freshMeta = this.meta.fresh ? await this.meta.fresh([path]) : await this.meta.get([path]);
2053
- if (freshMeta[0]?.link !== void 0) throw fsError("EEXIST", path);
2054
- if (freshMeta[0] !== void 0) await this.meta.remove([path]);
2063
+ const found = freshMeta[0];
2064
+ if (found?.link !== void 0) throw fsError("EEXIST", path);
2065
+ if (found !== void 0 && !found.vacated) await this.meta.remove([path]);
2055
2066
  this.forget(physical);
2056
2067
  this.refuse(physical);
2057
2068
  await dir.getDirectoryHandle(name2, { create: true });
@@ -2071,8 +2082,9 @@ var OpfsFs = class {
2071
2082
  await this.locked(physical, async () => {
2072
2083
  if (await this.handleExists(dir, name2)) throw fsError("EEXIST", path);
2073
2084
  const freshMeta = this.meta.fresh ? await this.meta.fresh([path]) : await this.meta.get([path]);
2074
- if (freshMeta[0]?.link !== void 0) throw fsError("EEXIST", path);
2075
- if (freshMeta[0] !== void 0) await this.meta.remove([path]);
2085
+ const found = freshMeta[0];
2086
+ if (found?.link !== void 0) throw fsError("EEXIST", path);
2087
+ if (found !== void 0 && !found.vacated) await this.meta.remove([path]);
2076
2088
  this.forget(physical);
2077
2089
  this.refuse(physical);
2078
2090
  await dir.getFileHandle(name2, { create: true });
@@ -2176,14 +2188,15 @@ var OpfsFs = class {
2176
2188
  const moving = await this.movedOwner(source);
2177
2189
  const carry = (entry) => ({
2178
2190
  ino: inodeOf(source.path),
2179
- ...entry,
2191
+ ...occupied(entry),
2180
2192
  ...moving,
2181
2193
  path: destination,
2182
2194
  ctimeMs: now
2183
2195
  });
2196
+ const left = vacatedAt(source.path);
2184
2197
  if (source.link) {
2185
2198
  if (target?.handle) await this.rm(destination);
2186
- await this.meta.move(source.path, destination, carry);
2199
+ await this.meta.move(source.path, destination, carry, left);
2187
2200
  this.touch(Date.now(), parentOf3(source.path), parent.path);
2188
2201
  return;
2189
2202
  }
@@ -2198,9 +2211,12 @@ var OpfsFs = class {
2198
2211
  () => this.place(source.handle, dir, staging)
2199
2212
  );
2200
2213
  if (swap) await this.rm(destination, { recursive: true });
2201
- await this.meta.move(source.path, destination, carry);
2214
+ await this.meta.move(source.path, destination, carry, left);
2202
2215
  this.movePending(source.path, destination);
2203
- if (!moved2) await this.rm(source.path, { recursive: true });
2216
+ if (!moved2) {
2217
+ await this.rm(source.path, { recursive: true });
2218
+ await this.meta.update(source.path, () => left);
2219
+ }
2204
2220
  let copied = !moved2;
2205
2221
  if (swap) {
2206
2222
  const staged = await dir.getDirectoryHandle(staging);
@@ -2285,11 +2301,11 @@ var OpfsFs = class {
2285
2301
  const fields = await this.newFields(at, owner && { uid: owner.uid, gid: owner.gid }, false);
2286
2302
  await this.entering(
2287
2303
  at,
2288
- () => this.meta.update(at, () => ({
2304
+ () => this.meta.update(at, (entry) => ({
2289
2305
  path: at,
2290
2306
  link: target,
2291
2307
  dir: parent.path,
2292
- ino: inodeOf(at),
2308
+ ino: entry?.vacated && entry.ino !== void 0 ? entry.ino : inodeOf(at),
2293
2309
  mtimeMs: now,
2294
2310
  ctimeMs: now,
2295
2311
  ...fields
@@ -2357,7 +2373,10 @@ var OpfsFs = class {
2357
2373
  }
2358
2374
  async reconcile() {
2359
2375
  let removed = 0;
2360
- for (const entry of await this.meta.all()) {
2376
+ const entries = await this.meta.all();
2377
+ const carried = new Set(entries.filter((e) => !e.vacated).map((e) => e.ino));
2378
+ for (const entry of entries) {
2379
+ if (entry.vacated && carried.has(inodeOf(entry.path))) continue;
2361
2380
  const real = await this.real(entry.path);
2362
2381
  const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf3(entry.path));
2363
2382
  if (!stale) continue;
@@ -6408,7 +6427,7 @@ function satisfies(version, range2) {
6408
6427
  }
6409
6428
 
6410
6429
  // src/version.ts
6411
- var KERNEL_VERSION = true ? "1.122.3" : "0.0.0-dev";
6430
+ var KERNEL_VERSION = true ? "1.123.1" : "0.0.0-dev";
6412
6431
  var OS_RELEASE = "6.0.0-slicc";
6413
6432
  function procVersion() {
6414
6433
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12551,17 +12570,28 @@ var FALLBACK_MEMORY = 4 * KIB * KIB * KIB;
12551
12570
  var PID_FILES = ["cmdline", "comm", "stat", "statm", "status"];
12552
12571
  var SYSTEM_FILES = ["loadavg", "meminfo", "stat", "uptime", "version"];
12553
12572
  var SYS_TREE = {
12554
- kernel: { osrelease: `${OS_RELEASE}
12555
- `, ostype: "Linux\n" }
12573
+ kernel: {
12574
+ osrelease: `${OS_RELEASE}
12575
+ `,
12576
+ ostype: "Linux\n",
12577
+ slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0}
12578
+ `
12579
+ }
12556
12580
  };
12581
+ function isSysFile(at) {
12582
+ return typeof at === "string" || typeof at === "function";
12583
+ }
12557
12584
  function sysEntry(parts2) {
12558
12585
  let at = SYS_TREE;
12559
12586
  for (const part of parts2) {
12560
- if (typeof at !== "object" || !Object.hasOwn(at, part)) return void 0;
12587
+ if (isSysFile(at) || !Object.hasOwn(at, part)) return void 0;
12561
12588
  at = at[part];
12562
12589
  }
12563
12590
  return at;
12564
12591
  }
12592
+ function sysText(file, listing) {
12593
+ return typeof file === "string" ? file : file(listing());
12594
+ }
12565
12595
  function commOf(info) {
12566
12596
  const name2 = info.argv[0] ?? "";
12567
12597
  return name2.slice(name2.lastIndexOf("/") + 1).slice(0, 15);
@@ -12797,7 +12827,9 @@ var ProcTree = class {
12797
12827
  }
12798
12828
  if (top === "sys") {
12799
12829
  const entry = sysEntry(parts2.slice(1));
12800
- if (typeof entry === "string") return { kind: "file", content: () => entry };
12830
+ if (typeof entry === "string" || typeof entry === "function") {
12831
+ return { kind: "file", content: () => sysText(entry, () => this.listing()) };
12832
+ }
12801
12833
  return entry && { kind: "dir", names: () => Object.keys(entry) };
12802
12834
  }
12803
12835
  if (top === "self") {
@@ -19254,6 +19286,7 @@ var WasmProcess = class {
19254
19286
  var WASM_PROCESS_INIT = "wasm-process-init";
19255
19287
  var WASM_PROCESS_EXIT = "wasm-process-exit";
19256
19288
  var WASM_PROCESS_ERROR = "wasm-process-error";
19289
+ var WASM_PROCESS_READY = "wasm-process-ready";
19257
19290
  var WASM_THREAD_SPAWN = "wasm-thread-spawn";
19258
19291
  var WASM_THREAD_INIT = "wasm-thread-init";
19259
19292
  var WASM_THREAD_EXIT = "wasm-thread-exit";
@@ -19261,6 +19294,8 @@ var WASM_MAX_THREADS = 256;
19261
19294
 
19262
19295
  // src/kernel/host.ts
19263
19296
  var CRASHED = 70;
19297
+ var NO_MEMORY = 126;
19298
+ var WASM_OOM = /Cannot allocate Wasm memory|WebAssembly\.Memory\(\): could not allocate memory/;
19264
19299
  var KILL_GRACE_MS = 2e3;
19265
19300
  var KILL_HARD_MS = 5e3;
19266
19301
  var descIds = /* @__PURE__ */ new WeakMap();
@@ -19526,7 +19561,22 @@ function threadInit(opts, program, sab, cred, thread, main) {
19526
19561
  function settleExit(exit, settle3, code) {
19527
19562
  void exit.catch(() => void 0).then(() => settle3(code & 255));
19528
19563
  }
19564
+ function readiness(abi) {
19565
+ const ready2 = Promise.withResolvers();
19566
+ if (abi === "js") ready2.resolve(true);
19567
+ return ready2;
19568
+ }
19569
+ function outOfMemory(opts, data) {
19570
+ if (data?.type !== WASM_PROCESS_ERROR || !WASM_OOM.test(String(data.message))) return false;
19571
+ if (!opts.onOom?.()) {
19572
+ opts.onError?.(
19573
+ `${opts.argv0}: cannot allocate memory for a new process (too many processes at once)`
19574
+ );
19575
+ }
19576
+ return true;
19577
+ }
19529
19578
  function spawnWasmProcess(opts) {
19579
+ const { promise: ready2, resolve: started } = readiness(opts.program.abi);
19530
19580
  const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
19531
19581
  const header3 = new Int32Array(sab, 0, SAB_HEADER_I32);
19532
19582
  const blocked2 = opts.fork?.wasi?.blocked ?? opts.blocked;
@@ -19579,6 +19629,7 @@ function spawnWasmProcess(opts) {
19579
19629
  const finish = (code, sig) => {
19580
19630
  if (done4) return;
19581
19631
  done4 = true;
19632
+ started(true);
19582
19633
  clocks.freeze();
19583
19634
  dying.clear();
19584
19635
  endedBy = sig ?? process.execTermsig;
@@ -19596,7 +19647,12 @@ function spawnWasmProcess(opts) {
19596
19647
  queueMicrotask(() => finish(...dying.status(code)));
19597
19648
  };
19598
19649
  const handle = (data) => {
19599
- if (data?.type === WASM_PROCESS_EXIT) {
19650
+ if (data?.type === WASM_PROCESS_READY) {
19651
+ started(true);
19652
+ } else if (outOfMemory(opts, data)) {
19653
+ started(false);
19654
+ finish(NO_MEMORY);
19655
+ } else if (data?.type === WASM_PROCESS_EXIT) {
19600
19656
  finish(...dying.status(data.code));
19601
19657
  } else if (data?.type === WASM_PROCESS_ERROR) {
19602
19658
  if (!dying.ending) opts.onError?.(String(data.message));
@@ -19670,6 +19726,7 @@ function spawnWasmProcess(opts) {
19670
19726
  return {
19671
19727
  pid: opts.pid,
19672
19728
  exited,
19729
+ ready: ready2,
19673
19730
  kill,
19674
19731
  signal,
19675
19732
  termsig: () => endedBy,
@@ -21264,6 +21321,63 @@ var SettlingChildren = class {
21264
21321
  }
21265
21322
  };
21266
21323
 
21324
+ // src/kernel/wasm-memories.ts
21325
+ var DEFAULT_CEILING = 112;
21326
+ var IDLE_MS = 1e4;
21327
+ var MARGIN = 16;
21328
+ var WasmMemories = class {
21329
+ live = 0;
21330
+ ceiling;
21331
+ waiters = /* @__PURE__ */ new Set();
21332
+ idleMs;
21333
+ constructor(ceiling, idleMs = IDLE_MS) {
21334
+ this.ceiling = ceiling;
21335
+ this.idleMs = idleMs;
21336
+ }
21337
+ count() {
21338
+ return { live: this.live, ceiling: this.ceiling ?? 0 };
21339
+ }
21340
+ ended() {
21341
+ this.live = Math.max(0, this.live - 1);
21342
+ this.progress();
21343
+ }
21344
+ learn() {
21345
+ this.ceiling = Math.max(1, this.live - 1);
21346
+ }
21347
+ near() {
21348
+ return this.ceiling !== void 0 && this.live + MARGIN >= this.ceiling;
21349
+ }
21350
+ async admit() {
21351
+ while (this.ceiling !== void 0 && this.live >= this.ceiling) {
21352
+ if (!await this.changed()) throw new SpawnError("EAGAIN");
21353
+ }
21354
+ this.live += 1;
21355
+ this.progress();
21356
+ }
21357
+ progress() {
21358
+ const waiters = [...this.waiters];
21359
+ this.waiters.clear();
21360
+ for (const wake of waiters) wake();
21361
+ }
21362
+ changed() {
21363
+ return new Promise((resolve2) => {
21364
+ const wake = () => {
21365
+ clearTimeout(timer);
21366
+ resolve2(true);
21367
+ };
21368
+ const timer = setTimeout(() => {
21369
+ this.waiters.delete(wake);
21370
+ resolve2(false);
21371
+ }, this.idleMs);
21372
+ this.waiters.add(wake);
21373
+ });
21374
+ }
21375
+ };
21376
+ function defaultCeiling() {
21377
+ const node = globalThis.process?.versions?.node;
21378
+ return node === void 0 ? DEFAULT_CEILING : void 0;
21379
+ }
21380
+
21267
21381
  // src/mount/driver.ts
21268
21382
  function ownerOf2(req) {
21269
21383
  const owner = {
@@ -23461,6 +23575,9 @@ function withScriptEnv(target, command) {
23461
23575
  ]
23462
23576
  };
23463
23577
  }
23578
+ function memoriesOf(given) {
23579
+ return new WasmMemories(given?.ceiling ?? defaultCeiling(), given?.idleMs);
23580
+ }
23464
23581
  function childHandle(handle) {
23465
23582
  return {
23466
23583
  pid: handle.pid,
@@ -23532,6 +23649,7 @@ var Launcher = class {
23532
23649
  jobs = new JobTable();
23533
23650
  locks;
23534
23651
  fifos = new FifoTable();
23652
+ memories;
23535
23653
  sems = new SemTable();
23536
23654
  settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
23537
23655
  ptys = new PtyTable(
@@ -23585,6 +23703,7 @@ var Launcher = class {
23585
23703
  terminals = 0;
23586
23704
  constructor(options) {
23587
23705
  this.usersMode = usersMode(options.users);
23706
+ this.memories = memoriesOf(options.wasmMemories);
23588
23707
  this.rootFs = options.fs;
23589
23708
  const peers = options.fs.peers?.();
23590
23709
  this.locks = kernelLocks(options.fs.rootName, peers);
@@ -24415,7 +24534,11 @@ var Launcher = class {
24415
24534
  }
24416
24535
  const done4 = this.heldUrl(program);
24417
24536
  try {
24418
- const handle = this.start({ ...req, cwd, program, argv0: target.argv0, args, env });
24537
+ const handle = await this.admitted(
24538
+ program.abi !== "js",
24539
+ req.fds,
24540
+ (fds, quietOom) => this.start({ ...req, fds, quietOom, cwd, program, argv0: target.argv0, args, env })
24541
+ );
24419
24542
  void handle.exited.finally(done4);
24420
24543
  return handle;
24421
24544
  } catch (err) {
@@ -24459,6 +24582,39 @@ var Launcher = class {
24459
24582
  if (req.tag !== void 0) this.tags.set(pid, req.tag);
24460
24583
  if (req.links !== void 0) this.links.set(pid, req.links);
24461
24584
  }
24585
+ async admitted(wasm, fds, start2) {
24586
+ if (!wasm) return start2(fds);
24587
+ for (; ; ) {
24588
+ await this.memories.admit().catch(async (err) => {
24589
+ await fds.closeAll();
24590
+ throw err;
24591
+ });
24592
+ const counted2 = (handle2) => {
24593
+ void handle2.exited.finally(() => this.memories.ended());
24594
+ return handle2;
24595
+ };
24596
+ if (!this.memories.near()) {
24597
+ try {
24598
+ return counted2(start2(fds));
24599
+ } catch (err) {
24600
+ this.memories.ended();
24601
+ throw err;
24602
+ }
24603
+ }
24604
+ const forked = fds.fork();
24605
+ const handle = await Promise.resolve().then(() => counted2(start2(forked, true))).catch(async (err) => {
24606
+ this.memories.ended();
24607
+ await forked.closeAll();
24608
+ throw err;
24609
+ });
24610
+ const fit = await (handle.ready ?? true);
24611
+ if (fit) {
24612
+ await fds.closeAll();
24613
+ return handle;
24614
+ }
24615
+ await handle.exited;
24616
+ }
24617
+ }
24462
24618
  start(req) {
24463
24619
  this.sampling();
24464
24620
  if (req.pgid !== void 0 && this.jobs.sessionOf(req.pgid) === void 0) {
@@ -24509,6 +24665,10 @@ var Launcher = class {
24509
24665
  ...this.prefixes.length > 0 && !stack.writer ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
24510
24666
  createWorker: this.createWorker,
24511
24667
  onError: req.report,
24668
+ onOom: () => {
24669
+ this.memories.learn();
24670
+ return req.quietOom === true;
24671
+ },
24512
24672
  spawner: this.spawner(pid, req.report),
24513
24673
  forker: this.forker(pid, req),
24514
24674
  kill: (target, sig, info) => this.kill(target, sig, this.credOf(pid), pid, info),
@@ -24517,7 +24677,8 @@ var Launcher = class {
24517
24677
  boot: this.boot,
24518
24678
  processes: this.list(this.credOf(pid)),
24519
24679
  load: this.loadAverage(),
24520
- cpu: this.cpuTotal()
24680
+ cpu: this.cpuTotal(),
24681
+ wasm: this.memories.count()
24521
24682
  }),
24522
24683
  sudo: (call, signal) => this.sudo(pid, call, signal),
24523
24684
  crontab: (call) => this.crontab(pid, call),
@@ -24854,9 +25015,10 @@ var Launcher = class {
24854
25015
  }
24855
25016
  forker(ppid, parent) {
24856
25017
  const { exec: _exec, pgid: _pgid, group: _group, pid: _pid, scrub: _scrub, ...image } = parent;
24857
- return async (state, fds) => childHandle(
24858
- this.start({
25018
+ return async (state, given) => {
25019
+ const fresh2 = (fds, quietOom) => this.start({
24859
25020
  ...image,
25021
+ quietOom,
24860
25022
  cwd: state.cwd ?? parent.cwd,
24861
25023
  fds,
24862
25024
  ppid,
@@ -24867,8 +25029,9 @@ var Launcher = class {
24867
25029
  cred: this.credOf(ppid),
24868
25030
  tag: this.tagOf(ppid),
24869
25031
  links: this.links.get(ppid)
24870
- })
24871
- );
25032
+ });
25033
+ return childHandle(await this.admitted(parent.program.abi !== "js", given, fresh2));
25034
+ };
24872
25035
  }
24873
25036
  keySignal(tty, fallback, sig, send) {
24874
25037
  const pgid = this.jobs.tcgetpgrp(tty, fallback);