@ai-ecoverse/slicc-kernel 1.124.1 → 1.124.3

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
@@ -645,7 +645,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
645
645
  - **The same file:** a lock is keyed by the file's device and inode, which both kernels read from the shared sidecar, so a rename by either tab keeps it. A file removed and created again at the same path has the same key, as it already has within one kernel.
646
646
  - **Versions:** the channel and Web Lock names carry a protocol version (`v1`), and a kernel ignores messages that aren't v1. So kernels on different protocol versions (two tabs across an update) don't share locks, as before #675.
647
647
  - **Not across tabs:** the imports module's whole-file `tryLock` (per kernel), and locks on other mounts.
648
- - **Another tab's writes** ([#959](https://github.com/ai-ecoverse/slicc-kernel/issues/959)): a program's names and stats are cached in its process, and its kernel refreshes them when the kernel itself changes a file. While another kernel shares the root (a second tab), the kernel marks that view foreign, and a program then looks names up and reads stats again on every look. So a program that keeps checking a file sees what the other tab wrote, synced and closed. A kernel alone caches as before. While another kernel shares the root, a kernel also tells it of each file it writes ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)). The other kernel then drops the clean contents of that file, both its own and its programs', so a descriptor tab B already has open reads tab A's new bytes on its next read, as Linux's shared page cache does. The notice is a message, so a read that races it by less than a message's delivery can still see the old bytes, as with any reader that doesn't lock.
648
+ - **Another tab's writes** ([#959](https://github.com/ai-ecoverse/slicc-kernel/issues/959)): a program's names and stats are cached in its process, and its kernel refreshes them when the kernel itself changes a file. While another kernel shares the root (a second tab), the kernel marks that view foreign, and a program then looks names up and reads stats again on every look. So a program that keeps checking a file sees what the other tab wrote, synced and closed. A kernel alone caches as before. Stats are always read fresh while the root is shared. An Emscripten program keeps the names it has looked up, directories and files ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)), until its next call that isn't by path: a read, a write, a poll, a sleep, a wait, a spawn, a look at the clock or a draw of random bytes. Within one run of path-only calls (lstat, chmod, readlink and the like) with none of those in between, an answer the program takes from its own node, such as the node's type, can lag behind the other tab until that next call, as a path call racing a rename does on Linux. A program can only learn that the other tab acted through one of those calls, a new process or a stat, and all of them refresh what it sees. While another kernel shares the root, a kernel also tells it of each file it writes ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)). The other kernel then drops the clean contents of that file, both its own and its programs', so a descriptor tab B already has open reads tab A's new bytes on its next read, as Linux's shared page cache does. The notice is a message, so a read that races it by less than a message's delivery can still see the old bytes, as with any reader that doesn't lock.
649
649
  - **`slicc.fcntl_lock(fd, cmd, flock_ptr) -> errno`** ([#722](https://github.com/ai-ecoverse/slicc-kernel/issues/722)): a WASIX program's `fcntl` record locks reach the same table as an Emscripten program's, so the two see each other's locks.
650
650
  - **cmd:** `12` `F_GETLK`, `13` `F_SETLK` or `14` `F_SETLKW`. Any other cmd is `EINVAL`.
651
651
  - **The struct:** musl's wasm32 `struct flock`, 32 bytes: `l_type` i16 at 0 (`0` read, `1` write, `2` unlock), `l_whence` i16 at 2 (`0` SET, `1` CUR, `2` END, resolved against the descriptor's offset or size), `l_start` i64 at 8, `l_len` i64 at 16 (`0` to EOF and beyond, negative before `l_start`), `l_pid` i32 at 24. The kernel reads and writes only those five fields and never the padding.
@@ -942,11 +942,14 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
942
942
  ### How many processes at once
943
943
 
944
944
  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:
945
- - **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.
945
+ - **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. A learned ceiling is never below 16, since an OOM at a low count means something outside it (another tab in the same renderer) filled the renderer. It's raised by 8 for each minute without another OOM, back to the default.
946
946
  - **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.
947
947
  - **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.
948
948
  - **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`.
949
- - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling>` (the ceiling 0 for none).
949
+ - **Below a learned ceiling,** a process that still runs out of memory is started again once another process exits, and the spawn fails with `EAGAIN` after 10 s with none.
950
+ - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling> <source>`. The source is `default`, `learned`, or `none` (with the ceiling 0).
951
+
952
+ `/proc/sys/kernel/slicc_shared_root` reads `1` while another kernel shares the root (a tab, or a second kernel in the page) and `0` otherwise. A peer whose kernel ends (its tab closes or reloads) stops counting at once: each kernel queues a request for every peer's alive Web Lock, and the grant tells it the peer has gone ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)). `/proc/sys/kernel/slicc_fs_calls` reads the number of path calls (lookups, stats, opens, creates, removals and the like) the kernel has served its programs since it started.
950
953
 
951
954
  ### Metadata
952
955
 
@@ -1002,6 +1005,8 @@ npm run test:unit
1002
1005
 
1003
1006
  `npm test` builds `dist/` and runs the integration tests in Chromium from `playwright-core`, over raw CDP through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a cross-origin isolated test page that installs bash and coreutils into OPFS. Each test writes screenshots, console logs and CPU profiles to `artifacts/`; V8 coverage from the page and every worker, mapped back to `src/` through the source maps, goes to `coverage/`, and `artifacts/hotspots.md` lists where the time went.
1004
1007
 
1008
+ The recorder behind that coverage changes timing and memory in long runs. It attaches to every page and worker with the debugger, profiler and precise coverage on, and pauses every process worker at its exit breakpoint (`exits` in `test/integration/chrome.mjs`) until the coverage of that worker is collected, keeping every collected script until the run ends. A run with thousands of process exits or hundreds of page loads therefore grows the test process by gigabytes, and each exit waits longer as it grows, so a parent can sit in `proc-exec` for tens of seconds while its child has already finished; each further page load also gets slower (in one tab reloaded 300 times, a boot went from 80 to 500 ms with no kernel in any other tab). When a long run looks stuck or leaky, measure it again with a `launch` that passes `coverage: ['/nothing/']` and no `exits` before blaming the kernel (#991).
1009
+
1005
1010
  Unit tests live in `test/unit/`, which stays out of git. They run real wasm bash and coreutils through the whole stack in Node `worker_threads` against an in-memory OPFS. The pre-commit hook runs them under coverage and requires every changed line in `src/` to be covered. `tools/heavy.sh` runs them two files at a time, in one of two unit slots of the host (`/tmp/slicc-unit-N.lock`, waiting while both are taken; a lock it can't take for any other reason, such as a lock file it can't open, ends the run with `lockf`'s status; `SLICC_UNIT_SLOTS` sets how many, `SLICC_UNIT_SLOT=0` takes none, as for CI; a HUP, INT or TERM to `heavy.sh` stops the run it holds or waits for), never in the browser runs' slots (those go through `slicc-heavy`; `SLICC_HEAVY_SLOT=1` runs the unit tests through `~/.bb/coord-bin/slicc-heavy` instead, when it is there), and stops the run if its processes use more than 3,000,000 KB together or 2,000,000 KB in one process (2,800,000 KB for the command itself, the coverage tool that merges the report at the end; `SLICC_HEAVY_ROOT_KB`, never below the per-process cap), or run longer than 900 s once it has its slot (`SLICC_HEAVY_TIMEOUT`; each test also has 180 s) (`SLICC_HEAVY_REPORT=1` prints the peak). `SLICC_HEAVY_TREE_KB` and `SLICC_HEAVY_PROC_KB` raise the caps, but only for measuring a run under a higher ceiling that still stops a runaway, never to get a commit through. The guard follows the command's process group and every process it has seen, including children that leave the group, but a process that forks, calls `setsid` and whose parent exits within one 0.25 s sample can escape: macOS has no unprivileged containment, and a test that daemonizes is a bug. The compiler tests (zig, rustc and cargo), each a few GB, are integration tests in `test/integration/compilers.test.mjs`, which CI runs.
1006
1011
 
1007
1012
  The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint-comments` (no comments anywhere), `slicc-no-unit-tests` (no unit tests in git) and `slicc-diff-cover` (100% coverage of changed lines) commands that `npm run lint` and the pre-commit hook use.
@@ -1012,6 +1017,8 @@ The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc
1012
1017
 
1013
1018
  `test/integration/fixtures/preh1stat/` is a tiny Emscripten program (`preh1stat.c`) that prints its `getuid`/`geteuid`/`getgid`/`getegid` and the owner of each path it's given. It's built **without** the credential library (`slicc_cred_js`), with today's emcc from homescoop's pinned `emsdk@0.4.0`, so it stands for the packages installed before H1. Those see every file as `1000:1000` (`stat-owner.test.mjs`), and every published `@ai-ecoverse/wasm-*` package now links the library, so no package can carry that row. Build it with `test/integration/fixtures/preh1stat/build.sh`: it fetches `emsdk@0.4.0` unless `emcc` is on `PATH`, `PREH1STAT_BUILD` sets its work directory, and it refuses a build whose glue carries the credential library.
1014
1019
 
1020
+ `test/integration/fixtures/clocklink/` is a tiny Emscripten program (`clocklink.c`) for `crowded-lookups.test.mjs` (#984). It creates the file named by its second argument (the signal to the other kernel), reads the link named by its first, then reads only the wall clock until the deadline given as its third argument (milliseconds since the epoch), reads the link again and prints both answers (`first=EINVAL second=t`). Build it with `test/integration/fixtures/clocklink/build.sh`, which fetches `emsdk@0.4.0` (emcc 4.0.23) unless `emcc` is on `PATH`; `CLOCKLINK_BUILD` sets its work directory. The committed build is `bin/clocklink` sha256 `0096e2ff68fb380d1b71aaa5fae64d96d6fe60aa2a3933e7207394d30436bf72` and `bin/clocklink.wasm` sha256 `e251dfa3b9d19d855cd0704a9cf67e7a7d28270083292ecde18809932bfbba16`.
1021
+
1015
1022
  `test/integration/fixtures/linktest/` is a WASIX program (`linktest.c`) that calls `linkat(AT_FDCWD, from, AT_FDCWD, to, AT_SYMLINK_FOLLOW)` and prints the new name's type, whether it shares the target's inode, and its link count, for `link-follow.test.mjs`. Coreutils' `ln -L` (Emscripten) takes another path, so the WASI program covers `path_link` with the follow flag. Build it with homescoop's pinned wasixcc and wasix-sysroot 2025.9.30-28: `INSTALL_WASIXCC=<homescoop>/scripts/install-wasixcc.sh WASIX_SYSROOT_28=<unpacked -28 package> test/integration/fixtures/linktest/build.sh`.
1016
1023
 
1017
1024
  `tools/big-file.mjs [bytes]` is a manual check, not part of any suite. It writes a file of that size (64 MiB by default) through a tmpfs mount with the unit-test launcher, reads it back, and prints the peak RSS and its ratio to the file size. A 256 MiB file peaks at about 1.8 times its size. Before ranged I/O (#127), a 1.1 GiB file peaked at 18 GB of RSS. **Do not run it past 1 GiB on a workstation.** It refuses anything over 256 MiB unless `SLICC_BIG_FILE_I_HAVE_THE_RAM=1` is set. Measure a small file and extrapolate instead. The unit suite checks a 32 MiB file the same way, and allows a fixed 192 MiB for garbage V8 has yet to collect, which dominates at that size.
@@ -1055,6 +1062,7 @@ The tools in `tools/cert-review/`:
1055
1062
 
1056
1063
  - **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.
1057
1064
  - **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. The gate reads the tool's stdout, and no tool passes the pull request's output there: suite pipes the tests' output into its log file, and compat prints only its own lines, so a test that prints `suite: PASS` can't stand in for a silent suite. A tool that let such output through would need its verdict on a separate descriptor. 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.
1065
+ - **Evidence comes by id from the job that made it:** the jobs that run the pull request's code (`pack candidate`, which builds it, and both `suite` legs, which run its tests) share the run's artifact store with the jobs that don't, and code in them can reach the runner's artifact token. So no consumer trusts a name or a pattern. Each producer job hands on its artifact's `artifact-id` and `artifact-digest` as job outputs, consumers download by `artifact-ids` with `digest-mismatch: error`, and no upload uses `overwrite`, so a second upload of a name fails instead of replacing it. The base build has its own job, `pack base`, which runs only main's code, so the candidate's job can't speak for it. `tools/cert-review/artifacts.mjs` lists the run's artifacts and fails when one in our namespaces (`kernel-tgz-`, `cert-review-`) wasn't uploaded by its producer, with that producer's id, name and digest, or when a producer's artifact is gone. `same` runs it on the two tarballs before it compares them, so the "not behaviour (identical dist)" skip can't be bought with a planted base tarball. `record` runs it on every artifact before it writes the line, and a failure there makes the record INCOMPLETE. **What this protects:** artifacts of jobs that run no pull-request code (the base build, the delta, kernel-compat, the same-dist evidence, the review's verdict and evidence) can't be forged or replaced. **What it doesn't:** the candidate's own artifacts, its tarball and both suites' results (the base leg runs the pull request's tests on main's tree, for the NEGATIVE), stay the pull request's by design, because code that runs on a runner can always lie about its own results. They're consumed by their exact names, at most one each. That's the same deliberate-malice residual as the workflow-rewrite one above.
1058
1066
  - **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.
1059
1067
  - **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.
1060
1068
  - **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:
@@ -856,6 +856,8 @@ var ProtectedFs = class {
856
856
  // src/fs/remote.ts
857
857
  var ANNOUNCE_WAIT_MS = 500;
858
858
  var PRUNE_EVERY_MS = 3e4;
859
+ var MOURN_TRIES = 5;
860
+ var MOURN_RETRY_MS = 100;
859
861
  function lockManager() {
860
862
  return globalThis.navigator?.locks;
861
863
  }
@@ -864,6 +866,7 @@ var RemoteWatch = class {
864
866
  name;
865
867
  tab = crypto.randomUUID();
866
868
  peers = /* @__PURE__ */ new Set();
869
+ mourned = /* @__PURE__ */ new Set();
867
870
  others = /* @__PURE__ */ new Map();
868
871
  union = /* @__PURE__ */ new Set();
869
872
  listeners = /* @__PURE__ */ new Set();
@@ -932,8 +935,7 @@ var RemoteWatch = class {
932
935
  receive(data) {
933
936
  if (data.hello !== void 0) this.greeted(data.hello);
934
937
  if (data.here !== void 0 && data.to === this.tab) {
935
- this.peers.add(data.here);
936
- this.recount();
938
+ this.arrived(data.here);
937
939
  this.met?.();
938
940
  }
939
941
  if (data.tab !== void 0 && data.paths !== void 0) this.announced(data);
@@ -942,9 +944,36 @@ var RemoteWatch = class {
942
944
  }
943
945
  if (data.changed !== void 0) this.emit({ paths: data.changed, trees: [] });
944
946
  }
945
- greeted(peer) {
947
+ arrived(peer) {
946
948
  this.peers.add(peer);
947
949
  this.recount();
950
+ void this.mourn(peer);
951
+ }
952
+ async mourn(peer, tries = MOURN_TRIES) {
953
+ if (!this.locks || this.mourned.has(peer)) return;
954
+ this.mourned.add(peer);
955
+ const name2 = this.lockOf(peer);
956
+ const { held = [], pending = [] } = await this.locks.query();
957
+ if (![...held, ...pending].some((lock) => lock.name === name2)) {
958
+ this.mourned.delete(peer);
959
+ if (tries > 1 && this.peers.has(peer)) {
960
+ setTimeout(() => void this.mourn(peer, tries - 1), MOURN_RETRY_MS);
961
+ }
962
+ return;
963
+ }
964
+ void this.locks.request(name2, async () => {
965
+ this.mourned.delete(peer);
966
+ this.others.delete(peer);
967
+ this.peers.delete(peer);
968
+ this.rebuild();
969
+ this.recount();
970
+ for (const wait2 of this.waiting.values()) {
971
+ if (wait2.left.delete(peer) && wait2.left.size === 0) wait2.done();
972
+ }
973
+ });
974
+ }
975
+ greeted(peer) {
976
+ this.arrived(peer);
948
977
  const answer = () => {
949
978
  this.channel.postMessage({ here: this.tab, to: peer });
950
979
  if (this.mine.length > 0) void this.announce(this.mine);
@@ -954,8 +983,7 @@ var RemoteWatch = class {
954
983
  }
955
984
  announced({ tab, paths, seq: seq2 }) {
956
985
  const from = tab;
957
- this.peers.add(from);
958
- this.recount();
986
+ this.arrived(from);
959
987
  if (paths.length > 0) this.others.set(from, paths);
960
988
  else this.others.delete(from);
961
989
  this.rebuild();
@@ -1567,7 +1595,8 @@ var OpfsFs = class {
1567
1595
  async linkAlong(path) {
1568
1596
  for (const entry of await this.meta.get(prefixes(path))) {
1569
1597
  if (entry?.link === void 0) continue;
1570
- if (!await this.real(entry.path) && await this.real(parentOf3(entry.path))) return entry;
1598
+ if (entry.path !== path && await this.real(entry.path)) continue;
1599
+ if (await this.real(parentOf3(entry.path))) return entry;
1571
1600
  }
1572
1601
  return void 0;
1573
1602
  }
@@ -2992,7 +3021,7 @@ var CdpFacade = class {
2992
3021
  for (const conn of this.connections) conn.close();
2993
3022
  }
2994
3023
  async acceptLoop() {
2995
- const served = /* @__PURE__ */ new Set();
3024
+ const served2 = /* @__PURE__ */ new Set();
2996
3025
  for (; ; ) {
2997
3026
  let conn;
2998
3027
  try {
@@ -3000,10 +3029,10 @@ var CdpFacade = class {
3000
3029
  } catch {
3001
3030
  break;
3002
3031
  }
3003
- const done4 = this.serve(conn).finally(() => served.delete(done4));
3004
- served.add(done4);
3032
+ const done4 = this.serve(conn).finally(() => served2.delete(done4));
3033
+ served2.add(done4);
3005
3034
  }
3006
- await Promise.allSettled([...served]);
3035
+ await Promise.allSettled([...served2]);
3007
3036
  }
3008
3037
  async serve(conn) {
3009
3038
  this.connections.add(conn);
@@ -6565,7 +6594,7 @@ function satisfies(version, range2) {
6565
6594
  }
6566
6595
 
6567
6596
  // src/version.ts
6568
- var KERNEL_VERSION = true ? "1.124.1" : "0.0.0-dev";
6597
+ var KERNEL_VERSION = true ? "1.124.3" : "0.0.0-dev";
6569
6598
  var OS_RELEASE = "6.0.0-slicc";
6570
6599
  function procVersion() {
6571
6600
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -8264,7 +8293,7 @@ var RealmProxy = class {
8264
8293
  for (const conn of this.connections) conn.close();
8265
8294
  }
8266
8295
  async acceptLoop() {
8267
- const served = /* @__PURE__ */ new Set();
8296
+ const served2 = /* @__PURE__ */ new Set();
8268
8297
  try {
8269
8298
  for (; ; ) {
8270
8299
  await this.slots.acquire(1, this.stop.signal);
@@ -8277,13 +8306,13 @@ var RealmProxy = class {
8277
8306
  }
8278
8307
  const done4 = this.serve(conn).finally(() => {
8279
8308
  this.slots.release(1);
8280
- served.delete(done4);
8309
+ served2.delete(done4);
8281
8310
  });
8282
- served.add(done4);
8311
+ served2.add(done4);
8283
8312
  }
8284
8313
  } catch {
8285
8314
  }
8286
- await Promise.allSettled([...served]);
8315
+ await Promise.allSettled([...served2]);
8287
8316
  }
8288
8317
  async serve(conn) {
8289
8318
  this.connections.add(conn);
@@ -12015,7 +12044,12 @@ var IDENTITY_OPS = /* @__PURE__ */ new Set([
12015
12044
  "chown",
12016
12045
  "lchown"
12017
12046
  ]);
12047
+ var served = 0;
12048
+ function servedFsCalls() {
12049
+ return served;
12050
+ }
12018
12051
  async function dispatchSyncFs(req) {
12052
+ served++;
12019
12053
  const entry = resolveSyncFsToken(req.token);
12020
12054
  if (!entry) return { ok: false, errno: "EACCES", message: "sync-fs: unknown or revoked token" };
12021
12055
  if (!req.identity || !IDENTITY_OPS.has(req.op)) return dispatchOne(entry, req);
@@ -12721,7 +12755,11 @@ var SYS_TREE = {
12721
12755
  osrelease: `${OS_RELEASE}
12722
12756
  `,
12723
12757
  ostype: "Linux\n",
12724
- slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0}
12758
+ slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0} ${listing.wasm?.source ?? "none"}
12759
+ `,
12760
+ slicc_shared_root: (listing) => `${listing.peers ?? 0}
12761
+ `,
12762
+ slicc_fs_calls: (listing) => `${listing.fsCalls ?? 0}
12725
12763
  `
12726
12764
  }
12727
12765
  };
@@ -21926,37 +21964,65 @@ var SettlingChildren = class {
21926
21964
 
21927
21965
  // src/kernel/wasm-memories.ts
21928
21966
  var DEFAULT_CEILING = 112;
21967
+ var CEILING_FLOOR = 16;
21929
21968
  var IDLE_MS = 1e4;
21930
21969
  var MARGIN = 16;
21970
+ var DECAY_MS = 6e4;
21971
+ var DECAY_STEP = 8;
21931
21972
  var WasmMemories = class {
21932
21973
  live = 0;
21933
- ceiling;
21974
+ learned;
21934
21975
  waiters = /* @__PURE__ */ new Set();
21976
+ base;
21935
21977
  idleMs;
21936
- constructor(ceiling, idleMs = IDLE_MS) {
21937
- this.ceiling = ceiling;
21978
+ now;
21979
+ constructor(ceiling, idleMs = IDLE_MS, now = Date.now) {
21980
+ this.base = ceiling;
21938
21981
  this.idleMs = idleMs;
21982
+ this.now = now;
21983
+ }
21984
+ ceiling() {
21985
+ const learned = this.learned;
21986
+ if (!learned) return this.base;
21987
+ const steps = Math.floor((this.now() - learned.at) / DECAY_MS);
21988
+ const raised = learned.ceiling + steps * DECAY_STEP;
21989
+ if (raised < (this.base ?? DEFAULT_CEILING)) return raised;
21990
+ this.learned = void 0;
21991
+ return this.base;
21939
21992
  }
21940
21993
  count() {
21941
- return { live: this.live, ceiling: this.ceiling ?? 0 };
21994
+ const ceiling = this.ceiling();
21995
+ const source = this.learned ? "learned" : ceiling === void 0 ? "none" : "default";
21996
+ return { live: this.live, ceiling: ceiling ?? 0, source };
21942
21997
  }
21943
21998
  ended() {
21944
21999
  this.live = Math.max(0, this.live - 1);
21945
22000
  this.progress();
21946
22001
  }
21947
22002
  learn() {
21948
- this.ceiling = Math.max(1, this.live - 1);
22003
+ const top = this.base ?? DEFAULT_CEILING;
22004
+ this.learned = {
22005
+ ceiling: Math.min(top, Math.max(CEILING_FLOOR, this.live - 1)),
22006
+ at: this.now()
22007
+ };
21949
22008
  }
21950
22009
  near() {
21951
- return this.ceiling !== void 0 && this.live + MARGIN >= this.ceiling;
22010
+ const ceiling = this.ceiling();
22011
+ return ceiling !== void 0 && this.live + MARGIN >= ceiling;
21952
22012
  }
21953
22013
  async admit() {
21954
- while (this.ceiling !== void 0 && this.live >= this.ceiling) {
22014
+ for (let ceiling = this.ceiling(); ceiling !== void 0 && this.live >= ceiling; ) {
21955
22015
  if (!await this.changed()) throw new SpawnError("EAGAIN");
22016
+ ceiling = this.ceiling();
21956
22017
  }
21957
22018
  this.live += 1;
21958
22019
  this.progress();
21959
22020
  }
22021
+ async refused() {
22022
+ const ceiling = this.ceiling();
22023
+ if (ceiling === void 0 || this.live >= ceiling) return;
22024
+ if (!await this.changed()) throw new SpawnError("EAGAIN");
22025
+ }
21960
22026
  progress() {
21961
22027
  const waiters = [...this.waiters];
21962
22028
  this.waiters.clear();
@@ -23061,7 +23127,7 @@ async function openHostfs(spec, connect2, timing = {}) {
23061
23127
  ...active?.capabilities.ranges === true ? { ranges: true } : {},
23062
23128
  ...active ? { maxIo: Math.min(active.maxIo, HOSTFS_IO) } : {}
23063
23129
  });
23064
- const served = serveFilesystem(
23130
+ const served2 = serveFilesystem(
23065
23131
  port2,
23066
23132
  { ...slot2.handlers, mount: ranged2 },
23067
23133
  {
@@ -23078,12 +23144,12 @@ async function openHostfs(spec, connect2, timing = {}) {
23078
23144
  up(client) {
23079
23145
  active = timed(client, timing.callMs ?? HOSTFS_CALL_MS);
23080
23146
  slot2.insert(client, handlers);
23081
- served.invalidate(true);
23147
+ served2.invalidate(true);
23082
23148
  },
23083
- event: (event) => served.invalidate(invalidation(event)),
23149
+ event: (event) => served2.invalidate(invalidation(event)),
23084
23150
  down() {
23085
23151
  slot2.eject();
23086
- served.invalidate(true);
23152
+ served2.invalidate(true);
23087
23153
  },
23088
23154
  ...timing
23089
23155
  },
@@ -25217,6 +25283,10 @@ var Launcher = class {
25217
25283
  return handle;
25218
25284
  }
25219
25285
  await handle.exited;
25286
+ await this.memories.refused().catch(async (err) => {
25287
+ await fds.closeAll();
25288
+ throw err;
25289
+ });
25220
25290
  }
25221
25291
  }
25222
25292
  start(req) {
@@ -25282,7 +25352,9 @@ var Launcher = class {
25282
25352
  processes: this.list(this.credOf(pid)),
25283
25353
  load: this.loadAverage(),
25284
25354
  cpu: this.cpuTotal(),
25285
- wasm: this.memories.count()
25355
+ wasm: this.memories.count(),
25356
+ peers: Number(this.locks.shared?.() === true),
25357
+ fsCalls: servedFsCalls()
25286
25358
  }),
25287
25359
  sudo: (call, signal) => this.sudo(pid, call, signal),
25288
25360
  crontab: (call) => this.crontab(pid, call),
@@ -25776,7 +25848,7 @@ var Launcher = class {
25776
25848
  const worker = this.createDriverWorker();
25777
25849
  const driver = new MessageChannel();
25778
25850
  const client = new MessageChannel();
25779
- const served = serveClient(client.port1, {
25851
+ const served2 = serveClient(client.port1, {
25780
25852
  launcher: async () => this,
25781
25853
  scope: "transport"
25782
25854
  });
@@ -25788,7 +25860,7 @@ var Launcher = class {
25788
25860
  port: driver.port1,
25789
25861
  dispose: () => {
25790
25862
  worker.terminate();
25791
- served.detach();
25863
+ served2.detach();
25792
25864
  driver.port1.close();
25793
25865
  },
25794
25866
  onCrash: (listener) => worker.addEventListener("error", (event) => {
@@ -25802,12 +25874,12 @@ var Launcher = class {
25802
25874
  const ask = (handle) => this.onMountPending?.({ target, source, ...handle ? { handle } : {} });
25803
25875
  const { port1, port2 } = new MessageChannel();
25804
25876
  const medium = removableMedium((handle) => {
25805
- served.invalidate(true);
25877
+ served2.invalidate(true);
25806
25878
  ask(handle);
25807
25879
  });
25808
- const served = serveFilesystem(port2, medium.handlers, FSA_CAPABILITIES);
25880
+ const served2 = serveFilesystem(port2, medium.handlers, FSA_CAPABILITIES);
25809
25881
  const id2 = source.slice("fsa:".length);
25810
- this.removable.set(target, { id: id2, medium, served });
25882
+ this.removable.set(target, { id: id2, medium, served: served2 });
25811
25883
  const stored = this.inserted.get(id2) ?? await this.media.get(id2);
25812
25884
  if (stored && await granted(stored)) medium.insert(stored);
25813
25885
  else ask(stored);
@@ -26818,7 +26890,7 @@ function serveKernel(port, deps) {
26818
26890
  }
26819
26891
  const { port1, port2 } = new MessageChannel();
26820
26892
  const held = locks ? await holdLock(locks) : void 0;
26821
- const served = serveClient(port1, {
26893
+ const served2 = serveClient(port1, {
26822
26894
  launcher: ready2,
26823
26895
  signal: signalNumber,
26824
26896
  ...user !== void 0 ? { user } : {},
@@ -26826,8 +26898,8 @@ function serveKernel(port, deps) {
26826
26898
  ...locks ? { locks } : {},
26827
26899
  ...held ? { lock: held } : {}
26828
26900
  });
26829
- clients.add(served);
26830
- void served.closed.then(() => clients.delete(served));
26901
+ clients.add(served2);
26902
+ void served2.closed.then(() => clients.delete(served2));
26831
26903
  return port2;
26832
26904
  }
26833
26905
  async function ready2() {