@ai-ecoverse/slicc-kernel 1.100.2 → 1.100.4

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
@@ -76,7 +76,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
76
76
  | `pid` | the session leader's pid |
77
77
  | `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
78
78
  | `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group. A key typed while a child of that group is still setting up is held until the child has finished, then sent to the group in the foreground at that point; this applies only to children of a job-control shell (a process that ignores `SIGTTOU`, as interactive shells do) and to the programs those children exec, so a program that starts a child without job control (Python's `subprocess`) gets the key at once. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp`, signal masks, and starting the program it execs (which then sets itself up too). The hold ends 2 s after the child's first call at the latest. A key typed while the foreground group is busy, not reading the terminal (a shell between reading a command line and starting it), goes to that group and also to the job the group starts next, once that job has taken the terminal; the second copy is dropped when the group reads the terminal again. So `^C` or `^Z` right after a command line reaches the new job, as it does on Linux, where starting a job takes microseconds rather than a worker start. For the same reason, a child that is still setting up cannot give the terminal to its own group once its parent has already given the terminal to that group and taken it back: on Linux the child's `tcsetpgrp` would have landed first, so the parent's later choice stands. Otherwise `tcsetpgrp` behaves as before. A `^C`, `^\` or `^Z` that a program with a handler for it takes between two system calls, rather than while waiting, ends the program's next wait in the kernel that would block (reading nothing yet, `select` or `poll` with a timeout, a blocking write, `waitpid`) with `EINTR`, once, as if the key had come during that wait. A zero-timeout `select` or `poll` does not use it up. Without this, an interactive shell whose `^C` landed while it was taking in the key before it would wait for the next key before showing a new prompt: readline acts on `^C` only when a read ends with `EINTR`. On Linux that window is the microseconds between two reads; here it is a few kernel calls per key. A signal sent with `kill` does not do this. |
79
- | `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
79
+ | `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group. As on Linux, setting the size the terminal already has, here or with a program's `TIOCSWINSZ`, sends no `SIGWINCH`. A program's `TIOCSWINSZ` that writes back the size it last read with `TIOCGWINSZ` is left out, so it can't undo a resize that came in between. Readline does that write-back every time it starts reading a line; on Linux the gap between the two calls is microseconds, here it's two kernel calls, wide enough for a resize to land in it and the shell to keep the old size (#740). |
80
80
  | `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
81
81
  | `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
82
82
  | `ended` | resolves with `{ status, signal }` at the same time: `status` as `exited`, and `signal` named (`'SIGKILL'`) only when a signal ended it |
@@ -310,7 +310,7 @@ On top of preview1 a WASI program gets:
310
310
  - A kernel holds at most 4,096 named ones (`ENOSPC`), and a process at most 256 open handles (`EMFILE`; a refused `O_CREAT` leaves no name behind).
311
311
  - `MAP_SHARED` mappings aren't shared between processes, so unnamed semaphores (`sem_init` with `pshared`) work only between a process's threads, and `multiprocessing.shared_memory` doesn't work.
312
312
  - **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`, before the function is called); without it, missing arguments are zero (one cut short keeps the bytes given, with zeros above them) and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, and `closure_free` gives them back for reuse and empties the slot in every thread; a static program gets `ENOTSUP`. `closure_prepare` puts a trampoline with the requested signature (any of `i32`, `i64`, `f32`, `f64`, `v128`) into such a slot, in every thread: each call stores its arguments below the stack pointer and calls the backing function as `backing(values, results, user_data)`, then returns what it wrote to `results`. The trampolines are small wasm modules made and compiled at run time, once per signature in each thread (an instance per closure), so a page whose Content Security Policy forbids compiling WebAssembly (no `'wasm-unsafe-eval'`) cannot run them; it cannot run the program either. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
313
- - **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. An Emscripten program stops at its next kernel call as well, and at its next write, read or sleep that its glue answers without the kernel (a write to its own `/dev/null`, as in `yes > /dev/null`), so a stopped one keeps no core busy unless it computes without any of these calls. A stopped program doesn't move on while stopped: a `sleep` (or a blocked read) that would end meanwhile ends after `SIGCONT`. A signal for such a program is also delivered at those glue-answered calls, so a handler runs in a loop that only writes to `/dev/null`. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. A signal that arrives while a thread computes is thus delivered at its next call; when that call is a wait (`poll_oneoff` with a timeout or a descriptor, so `nanosleep` and `poll`/`select` too, `sigmask_poll`, or a futex wait), the handler has run during the call, so the wait ends at once: `EINTR` for a libc of -22 or later (an older one sees its clocks fire, as for any interruption), and a futex wait returns woken. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
313
+ - **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. An Emscripten program stops at its next kernel call as well, and at its next write, read or sleep that its glue answers without the kernel (a write to its own `/dev/null`, as in `yes > /dev/null`), so a stopped one keeps no core busy unless it computes without any of these calls. A stopped program doesn't move on while stopped: a `sleep` (or a blocked read) that would end meanwhile ends after `SIGCONT`. A call that the stop interrupted runs again after `SIGCONT`, as on Linux. One with a timeout (`select`, `poll`, `ppoll`, `pselect`, `nanosleep`, a timed semaphore wait) waits only for the time that's left, counted from when the kernel first got the call, so it still ends at its deadline. `epoll_wait` and `epoll_pwait` aren't run again: they fail with `EINTR`, even without a handler (signal(7)). A signal for such a program is also delivered at those glue-answered calls, so a handler runs in a loop that only writes to `/dev/null`. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. A signal that arrives while a thread computes is thus delivered at its next call; when that call is a wait (`poll_oneoff` with a timeout or a descriptor, so `nanosleep` and `poll`/`select` too, `sigmask_poll`, or a futex wait), the handler has run during the call, so the wait ends at once: `EINTR` for a libc of -22 or later (an older one sees its clocks fire, as for any interruption), and a futex wait returns woken. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
314
314
  - **Blocked signals** (`sigprocmask`, `pthread_sigmask`, `sigpending`): a program whose libc imports `slicc.sigmask_set(how, set_lo, set_hi, old_ptr)` and `slicc.sigpending_get(ptr)` (wasix-sysroot 2025.9.30-28 and later) has a blocked mask per thread. `how` is musl's `SIG_BLOCK` 0, `SIG_UNBLOCK` 1 or `SIG_SETMASK` 2, the set is the 64-bit `sigset_t` (bit `sig - 1`), and the old mask is written as a u64 at `old_ptr`; any other `how` is `EINVAL`. `SIGKILL` and `SIGSTOP` cannot be blocked. A blocked signal is not delivered, does not interrupt a sleep or a blocked call, and its default action (terminate or stop) waits: it stays pending, `sigpending` shows it (the thread's pending signals and the process's), and it is delivered when it is unblocked, before `sigmask_set` returns. A signal ignored with `SIG_IGN` is discarded even while blocked. A forked child (with the mask of the thread that forked), the program an `exec` starts and a spawned child keep the mask, all 64 bits of it, and so does a new thread; `/proc/<pid>/status` shows the main thread's as `SigBlk`. The mask is kept in a word of the worker's shared header, so changing it costs no kernel call. A program without the import (an older libc) has no mask and gets every signal at once, as before, and one it starts by `exec` with a mask set starts with none. A signal for the whole process goes to the main thread, even if the main thread blocks it and another thread does not. A signal sent to one thread (`pthread_kill`) that it blocks waits for that thread, and if it has no handler, the thread's unblock ends the whole process by the signal, whatever the other threads block. `slicc.sigmask_poll(set_lo, set_hi, in_ptr, out_ptr, nsubscriptions, nevents_ptr)` is `pselect`, `ppoll` and `sigsuspend`: it swaps the thread's mask to the given set for the call and puts the old one back before it returns. It delivers a pending signal the new mask unblocks, without polling, or polls as `poll_oneoff` does, under the new mask, so a signal it unblocks interrupts the wait with no window between the swap and the wait; with no subscriptions it waits until a signal is delivered. An interrupted call returns `EINTR` with no events, after the handler has run.
315
315
  - **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
316
316
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
@@ -899,13 +899,14 @@ Releases are cut by semantic-release on every push to `main`, in two jobs, and o
899
899
  - **plan:** works out the version, the previous tag, the commit and the base (`base.mjs`). The base is the newest earlier release whose `cert-review` commit status is `success`, or, until there is one, the release `base.json` names (the cert lane's last line). CI never reads the npm `certified` dist-tag.
900
900
  - **delta** (`delta.mjs`): lists the pull requests merged between the two tags (their merge commits, plus `pr` for a planted branch). Each gets its conventional-commit kind (`fix`, `feat`, `perf` and `revert` are behaviour changes) and the integration test files and titles it adds or changes. The titles are read from the pull request's patch: the added `test(…)` lines and the hunk headers git puts above changed lines. Escapes are decoded as JavaScript does, and a title git cut short, or a template literal, matches as a prefix.
901
901
  - **suite** (`suite.mjs`, `reporter.mjs`) runs on the bench pool, without secrets. It takes the tag's tree with its own dependencies and Chromium, and replaces `dist/` with the **published npm tarball** (for a planted branch, its own `npm pack`). The test command runs in a session of its own (`detached`), and `suite.mjs` notes a `SIGINT` (`ignored` in its summary) instead of dying of it. That way a stray signal to the step's process group can't cut the run short, while a `SIGTERM` (a real cancel) still ends it.
902
- - The release job runs the release's whole `test/integration/` suite, then the delta's test files again on the **previous release's dist** (the NEGATIVE).
902
+ - The release job runs the release's whole `test/integration/` suite, then each fix's test files again on the dist of **the newest published release before the fix was merged** (the NEGATIVE). `delta.mjs` groups the files by that baseline: for most fixes it is the previous release, but a fix for a bug that shipped inside the delta (a regression in v1.99.1 fixed for v1.99.2) is checked against the release that had the bug.
903
903
  - The base job runs the base release's own suite on its own published dist.
904
904
  - Each run uses the tree's own `npm test` command, with a reporter that records every test and subtest with its status and error.
905
905
  - **kernel-compat** (`compat.mjs`) supplies the base-set and catalog evidence: homescoop's kernel-compat run for the release.
906
906
  - Release runs share one group and intermediate ones are cancelled. When there is no finished run for the release commit, it dispatches `kernel-compat.yml` for that version (whose run is named `Kernel compat v<version>`) and waits up to 150 minutes.
907
907
  - It reads the run's report artifact, and **re-runs every regressing package once** (a dispatch with that `set`).
908
908
  - A regression that doesn't reproduce is a `compat-flake`; one that does is a `compat-regression`. A crash that reproduces and doesn't happen on the reference kernel is a `compat-crash`, which record FAILs in code.
909
+ - A run that failed with no regression in its report is a `compat-incomplete`, missing evidence and not a regression. It names the shards that left no report (for example, a shard that lost its bench runner), and those shards' failed jobs are re-run once first.
909
910
  - **review:** `findings.mjs` decides in code, per pull request:
910
911
  - `negative` (a fix's test fails on the previous dist and passes on the release) or `passes` (a feature's test passes);
911
912
  - otherwise a finding: `no-test`, `no-negative`, `fails-on-release`, `not-run`, `prev-not-run` or `no-titles`.
@@ -1007,6 +1008,12 @@ Two builds of one C program (`forktls.c` with `tlsvars.c`), a position-independe
1007
1008
 
1008
1009
  **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with homescoop's pinned `wasixcc` 0.4.7 and binaryen's `wasm-opt` 133 on `PATH` (the same toolchain rebuilds `bin/piefork.wasm` byte for byte). `bin/forktls-pie.wasm` is 816,066 bytes, sha256 `c5ed6b06036f4d3c42c660fc13ca1a995d647f11021829f2169567574254fd9e`; `bin/forktls-static.wasm` is 45,564 bytes, sha256 `5df019d5f2a4432132c8b7c6f5992d18ecf9d8e4c4be21351acd59c46c3ca48d`; `lib/libtlsside.so` is 736 bytes, sha256 `ecd32e561c051576a99e87caee5e1aca5238c4055e2e551065fa10fe7476d0b5`.
1009
1010
 
1011
+ ### `stopwait`
1012
+
1013
+ A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child 50 ms after it's ready and continues it 200 ms later. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
1014
+
1015
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions) and binaryen's `wasm-opt` (`--asyncify` for `proc_fork`) on `PATH`. `bin/stopwait.wasm` is 50,567 bytes, sha256 `570cb3e29cfdc083bbdc0d20707f2c07b087fe23cef13a8861a4a42b02d20f5f`.
1016
+
1010
1017
  ### `semtest`
1011
1018
 
1012
1019
  A WASIX C program for `test/integration/semaphore.test.mjs` (#717). It calls the `slicc.sem_*` imports directly (`import_module("slicc")`), so the rows don't wait for the libc side. `semtest MODE` runs one of:
@@ -12114,7 +12114,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12114
12114
  var EARLY_WHOLE_FILE = 64 * 1024;
12115
12115
 
12116
12116
  // src/version.ts
12117
- var KERNEL_VERSION = true ? "1.100.2" : "0.0.0-dev";
12117
+ var KERNEL_VERSION = true ? "1.100.4" : "0.0.0-dev";
12118
12118
  var OS_RELEASE = "6.0.0-slicc";
12119
12119
  function procVersion() {
12120
12120
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -14017,6 +14017,7 @@ var KernelTty = class {
14017
14017
  this.wake();
14018
14018
  }
14019
14019
  resize(cols, rows) {
14020
+ if (cols === this.cols && rows === this.rows) return;
14020
14021
  this.setSize(cols, rows);
14021
14022
  this.signal(SIG.WINCH);
14022
14023
  }
@@ -14391,6 +14392,9 @@ function terminalOf(ctx, fd) {
14391
14392
  }
14392
14393
  var json4 = (value) => ({ ok: true, kind: "json", json: value });
14393
14394
  var done3 = { ok: true, kind: "void" };
14395
+ function sizeKey(rows, cols) {
14396
+ return `${rows}x${cols}`;
14397
+ }
14394
14398
  function ptySyscall(req, ctx) {
14395
14399
  switch (req.op) {
14396
14400
  case "pty-open": {
@@ -14423,9 +14427,11 @@ function ptySyscall(req, ctx) {
14423
14427
  case "pty-packet":
14424
14428
  masterOf(ctx, req.fd).packet = req.on;
14425
14429
  return done3;
14426
- case "pty-winsz-set":
14427
- terminalOf(ctx, req.fd).resize(req.cols, req.rows);
14430
+ case "pty-winsz-set": {
14431
+ const tty = terminalOf(ctx, req.fd);
14432
+ if (ctx.sizesRead?.get(tty) !== sizeKey(req.rows, req.cols)) tty.resize(req.cols, req.rows);
14428
14433
  return done3;
14434
+ }
14429
14435
  }
14430
14436
  }
14431
14437
 
@@ -15807,6 +15813,10 @@ function identityKey(st, path) {
15807
15813
  }
15808
15814
  var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
15809
15815
  var HOSTS_ONLY = new Resolver({ routes: new Routes() });
15816
+ function timeLeft2(req, started) {
15817
+ if (!("timeoutMs" in req) || typeof req.timeoutMs !== "number" || req.timeoutMs < 0) return req;
15818
+ return { ...req, timeoutMs: Math.max(0, req.timeoutMs - (performance.now() - started)) };
15819
+ }
15810
15820
  var WasmProcess = class {
15811
15821
  exited = false;
15812
15822
  children;
@@ -15965,24 +15975,31 @@ var WasmProcess = class {
15965
15975
  for (const listener of this.stateListeners) listener("continued", SIG.CONT);
15966
15976
  }
15967
15977
  blocks = /* @__PURE__ */ new WeakMap();
15978
+ sizesRead = /* @__PURE__ */ new WeakMap();
15968
15979
  async syscall(req, cancelled, block) {
15969
15980
  this.options.onSyscall?.(req);
15981
+ const started = performance.now();
15982
+ let restarted = false;
15970
15983
  for (; ; ) {
15971
15984
  await this.resumed;
15972
15985
  if (this.dying) return DYING;
15986
+ const current = restarted ? timeLeft2(req, started) : req;
15973
15987
  const stops = this.stops;
15974
15988
  const scope = new AbortController();
15975
15989
  const forward = () => scope.abort();
15976
15990
  if (cancelled?.aborted) scope.abort();
15977
15991
  cancelled?.addEventListener("abort", forward, { once: true });
15978
15992
  if (block) this.blocks.set(scope.signal, block);
15979
- const result = await this.dispatch(req, scope.signal).finally(() => {
15993
+ const result = await this.dispatch(current, scope.signal).finally(() => {
15980
15994
  cancelled?.removeEventListener("abort", forward);
15981
15995
  this.waiting.delete(scope.signal);
15982
15996
  });
15983
15997
  if (this.dying) return DYING;
15984
15998
  const restart = !result.ok && result.errno === "EINTR" && this.stops !== stops;
15985
- if (restart && !this.options.hasPending?.()) continue;
15999
+ if (restart && !this.options.hasPending?.() && req.op !== "epoll-wait") {
16000
+ restarted = true;
16001
+ continue;
16002
+ }
15986
16003
  await this.resumed;
15987
16004
  return result;
15988
16005
  }
@@ -16029,7 +16046,17 @@ var WasmProcess = class {
16029
16046
  const owner = this.options.owners ? this.ttyOwner() : void 0;
16030
16047
  const guard2 = this.guarded();
16031
16048
  const { outputFilter } = this.options;
16032
- const ctx = { pid: this.pid, fds: this.fds, ptys, jobs, guard: guard2, owner, outputFilter };
16049
+ const sizesRead = this.sizesRead;
16050
+ const ctx = {
16051
+ pid: this.pid,
16052
+ fds: this.fds,
16053
+ ptys,
16054
+ jobs,
16055
+ guard: guard2,
16056
+ owner,
16057
+ outputFilter,
16058
+ sizesRead
16059
+ };
16033
16060
  return ptySyscall(req, ctx);
16034
16061
  }
16035
16062
  return await this.procSyscall(req, cancelled);
@@ -16665,8 +16692,11 @@ var WasmProcess = class {
16665
16692
  case "tty-set":
16666
16693
  tty.tcsets(req.termios, req.flush === true);
16667
16694
  return { ok: true, kind: "void" };
16668
- case "tty-winsz":
16669
- return { ok: true, kind: "json", json: tty.winsize() };
16695
+ case "tty-winsz": {
16696
+ const size = tty.winsize();
16697
+ this.sizesRead.set(tty, sizeKey(...size));
16698
+ return { ok: true, kind: "json", json: size };
16699
+ }
16670
16700
  case "tty-pgrp-get":
16671
16701
  return { ok: true, kind: "json", json: jobs ? jobs.tcgetpgrp(tty, this.sid()) : this.pid };
16672
16702
  case "tty-pgrp-set":