@ai-ecoverse/slicc-kernel 1.143.2 → 1.143.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
@@ -202,11 +202,11 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
202
202
  kernel.terminate();
203
203
  ```
204
204
 
205
- `createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache, warmWorker })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal`, `users`, `setRoutes`, `updateTransportTraits` and `terminate` as above, plus `integrity.revoke(path?)`, `root`, `writeFile(path, data, { mode }?)` (creating the parent directories; `mode` sets the permission bits, `0` to `0o7777`, before the data is written), `chmod(path, mode)` and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
205
+ `createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache, warmWorker, warmWorkers })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal`, `users`, `setRoutes`, `updateTransportTraits` and `terminate` as above, plus `integrity.revoke(path?)`, `root`, `writeFile(path, data, { mode }?)` (creating the parent directories; `mode` sets the permission bits, `0` to `0o7777`, before the data is written), `chmod(path, mode)` and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
206
206
 
207
207
  `compileCache` (opt-in, off by default; #1285, slicc-jsh#566) is an absolute host directory where each process worker keeps the compiled code of the JS programs it imports (`module.enableCompileCache`), so the next spawn of the same program skips V8's compile. It stores V8's code cache, keyed and checksummed by the module's source, one file per program (about 7 MB for a 2.4 MB bundle), shared by every kernel given the same directory; programs can't reach or write it (the worker hands it to Node before the lockdown, and `node:worker_threads` stays refused). Measured on a 2.4 MB bundle, a spawn went from about 54 to 39 ms; a trivial program costs about 1 ms more. **The directory must be private to the embedder:** V8's code cache is trusted bytecode that Node only checksums, so whoever can write it can plant code that runs inside a process worker past the lockdown. Never make it reachable from inside the kernel (no `hostfs` mount of it or of a parent), and don't share one between kernels of different trust. A host's own `NODE_COMPILE_CACHE` is never handed to the process workers, with or without the option, so programs never land in a host-wide cache. If a host preload (`--import` or `--require`) has already enabled another compile cache in the workers, a kernel given `compileCache` refuses to run programs there instead of caching them in the host's directory.
208
208
 
209
- `warmWorker` (on by default; `false` turns it off; #1341, slicc-jsh#566) keeps one process worker booted ahead of the next spawn, fork or thread on the Node entry, so the spawn skips the worker's boot: it has loaded the process worker module and nothing else, no pid, credentials, environment, cwd, descriptors, lockdown or program, all of which arrive with its init message, and every worker still serves exactly one process or thread and is then terminated. Its own `process.env` is a snapshot taken when the spare is created rather than at the spawn, but a program never sees it (`process` is withheld, and a program's environment is the one its spawn gives it). The spare never keeps the host alive, is replaced after each spawn off the critical path, is dropped if it errors or exits while idle, and is released after 30 s without a spawn; `terminate()` ends it. It costs about 11 MB of host RSS while it waits.
209
+ `warmWorker` (on by default; `false` turns it off; #1341, slicc-jsh#566) keeps one process worker booted ahead of the next spawn, fork or thread on the Node entry, so the spawn skips the worker's boot: it has loaded the process worker module and nothing else, no pid, credentials, environment, cwd, descriptors, lockdown or program, all of which arrive with its init message, and every worker still serves exactly one process or thread and is then terminated. Its own `process.env` is a snapshot taken when the spare is created rather than at the spawn, but a program never sees it (`process` is withheld, and a program's environment is the one its spawn gives it). The spare never keeps the host alive, is replaced after each spawn off the critical path, is dropped if it errors or exits while idle, and is released after 30 s without a spawn; `terminate()` ends it. It costs about 11 MB of host RSS while it waits. `warmWorkers: n` (opt-in; #1361, slicc-jsh#566) keeps up to `n` spares instead of one (an integer from 0 to 64; `warmWorker: false` still means none), so a burst of starts, as worker pools make, finds a spare for each: all are refilled after each take off the critical path, and all are released together after 30 s without a spawn and ended by `terminate()`. Each costs about 7–11 MB of host RSS while it waits. It pays off for bursts of `Worker` threads (jsh measured about 13 ms less per start in a burst of 8); for bursts of child processes that boot a large runtime of their own, the refills compete for CPU with the children and the gain is small or none.
210
210
 
211
211
  By default `nodeTransport()` refuses destinations on the host itself, because Node can reach them and a page can't: local DevTools on 9222–9229, dev servers, local admin APIs, and the cloud metadata address.
212
212
  - **What it refuses:** loopback (`127.0.0.0/8`, `::1`, and their IPv4-mapped forms), unspecified addresses (`0.0.0.0/8`, `::`), `localhost` and `*.localhost`, and link-local addresses (`169.254.0.0/16`, `fe80::/10`), plus AWS's IPv6 metadata address `fd00:ec2::254`.
@@ -370,9 +370,10 @@ The module's default export (or its `main` export) is called with a context and
370
370
  - `fs`: `stat`, `lstat`, `exists`, `readdir`, `mkdir` (with parents, or, with `{ recursive: false }`, as Node's `fs.mkdir` without `recursive`: `EEXIST` for an existing entry and `ENOENT` for a missing parent, atomically among the processes of one kernel, so it can serve as a lock there, but not between two kernels on one origin, such as two tabs ([#519](https://github.com/ai-ecoverse/slicc-kernel/issues/519)); `sync.fs.mkdir` takes the same option), `rm` (recursive), `unlink`, `rename`, `symlink`, `readlink`, `statfs`, `chmod(path, mode)`, and `readFile`/`writeFile` for small files. `chmod` takes the permission bits (`0` to `0o7777`, `EINVAL` otherwise) and follows a symlink; as `chmod(2)`, only the file's owner or root may (`EPERM`), so a program can make a script it wrote runnable by path. `sync.fs.chmod` is the same, synchronously. `statfs(path)` answers `{ type, bsize, blocks, bfree, bavail, files, ffree, quota, usage }`: the fields of Node's `StatFs`, plus `quota` and `usage` in bytes (what jsh 0.17 reads). The values come from the same source as an Emscripten program's `statfs`/`df`: the driver of the mount the path is on, or, on the root file system, the origin's storage estimate (`navigator.storage.estimate()`). `bsize` is 4096, `bfree` and `bavail` are what's left of the quota, and `type`, `files` and `ffree` are 0, since the kernel doesn't know them. Where there's no estimate (the Node entry's root), every count is 0. A missing path is `ENOENT`. `sync.fs` has it too. `rm` removes a whole tree in one kernel call (on the OPFS root, one recursive removal and one metadata delete: 50,000 files take about 3 s in Chrome, where `rm -r` takes about 25 s) with the result of a file-by-file removal: symlinks inside the tree are removed, never followed; a tree holding a protected path is refused with `EROFS` and nothing is removed; a mount inside the tree is emptied and its mount point answers `EBUSY`, while the rest is removed; under `users`, what the user may remove is removed and the call then fails with the first `EACCES`/`EPERM`; files a process holds open stay readable to it;
371
371
  - **`fs.watch(path, options?, onEvent)`** resolves with `{ close() }` ([#1031](https://github.com/ai-ecoverse/slicc-kernel/issues/1031)). `sync.fs.watch` returns the watcher directly and is in place when it returns, so it sees every change made after the call. This is the shape for Node's synchronous `fs.watch`.
372
372
  - **What it reports:** a file reports its own changes. A directory reports its direct entries, or with `{ recursive: true }`, everything below it.
373
- - **Events:** each one is `{ type, filename }`, in the order the operations happened.
373
+ - **Events:** each one is `{ type, filename, seq }`, in the order the operations happened.
374
374
  - `'rename'`: an entry appeared or went away (create, `mkdir`, `unlink`, `rm`, both names of a `rename`, `link`, `symlink`).
375
- - `'change'`: an existing entry's data or metadata changed (a write, `truncate`, `chmod`, `chown`, `utimes`). A write that creates a file gives `'rename'` and then `'change'`, as on Linux.
375
+ - `'change'`: an existing entry's data or metadata changed (a write, `truncate`, `chmod`, `chown`, `utimes`). A write that creates a file gives `'rename'` and then `'change'`, as on Linux. Writing a whole file over an existing one is a truncate and a write, so it gives two `'change'`s, or one if the new content is empty ([#1357](https://github.com/ai-ecoverse/slicc-kernel/issues/1357)).
376
+ - `seq` numbers the operations across the whole kernel, increasing. One operation heard by several watches (a directory's and a file's in it) has the same `seq` in each, so a program with several watches can merge their events back into one queue in operation order and, as inotify does, merge only events that are adjacent in it. `{ overflow: true }` and `{ ended: true }` carry the `seq` of the operation that caused them.
376
377
  - `filename` is the entry's name relative to the watched directory (`sub/a.txt` when recursive), or the file's basename for a file watch.
377
378
  - **Overflow:** a watch holds 1,000 events between reads. Beyond that, the rest are dropped, and `{ overflow: true }` follows the ones kept.
378
379
  - **The watched path itself:** like inotify, a watch follows the file or directory it was opened on, not its name. When a directory above it is renamed, the watch stays on the moved path with no event, and later events keep the name it was opened with. When the path itself is renamed, the watch gets one `{ type: 'rename', filename, moved: true }` and stays on the new path. Only a removal ends it: `rm` of the path or a directory above it, or a rename of something else over it gives one `'rename'` with its own name, then `{ ended: true }`. Renames are followed as this kernel makes them ([#1356](https://github.com/ai-ecoverse/slicc-kernel/issues/1356)). `close()` is safe at any time and idempotent, and nothing is delivered after it returns.
@@ -547,7 +548,7 @@ The module's default export (or its `main` export) is called with a context and
547
548
  - **The bound:** the server the value goes to is the one the vault rule names, verified against the kernel's own roots. Substitution trusts that server not to reflect the value back in a form the scrub doesn't know (encrypted under a key the program chose, say). That's the same residual as A1's.
548
549
  - **Elsewhere:** a placeholder written into any other A2 session reaches the server as written. It isn't the secret, so nothing leaks.
549
550
  - **Limits:** no revocation checking (neither OCSP nor CRLs, as with Node's default), no session resumption, and the clock is the page's `Date.now()`.
550
- - `net.resolve(name, { family, signal })` asks the kernel's resolver, which WASIX `resolve` and Emscripten `getaddrinfo` use too: entries in a real `/etc/hosts`, the kernel's own names (`localhost`, the node name, `host.slicc.internal`) and then the uplink's DNS ([#913](https://github.com/ai-ecoverse/slicc-kernel/issues/913)). It answers the addresses, in the resolver's order, for `family` `4`, `6` or `0` (both, IPv4 first; the default). A name with no answer rejects with a `JsCallError` of code `ENOTFOUND`, as Node's `dns.lookup` does, another family with `EAI_FAMILY`, and an aborted `signal` with its reason.
551
+ - `net.resolve(name, { family, signal })` asks the kernel's resolver, which WASIX `resolve` and Emscripten `getaddrinfo` use too: entries in a real `/etc/hosts`, the kernel's own names (`localhost`, the node name, `host.slicc.internal`) and then the uplink's DNS ([#913](https://github.com/ai-ecoverse/slicc-kernel/issues/913)). It answers the addresses, in the resolver's order, for `family` `4`, `6` or `0` (both, IPv4 first; the default). A name with no answer rejects with a `JsCallError` of code `ENOTFOUND`, as Node's `dns.lookup` does. A lookup the uplink fails or doesn't answer within 10 s rejects with `EAI_AGAIN` (the kernel's `sock-resolve` is `ETIMEDOUT` there, `ENOENT` for no answer), as Node's does for a transient failure, so a client can retry; the failure isn't cached ([#1363](https://github.com/ai-ecoverse/slicc-kernel/issues/1363)). Another family rejects with `EAI_FAMILY`, and an aborted `signal` with its reason.
551
552
  - `websocket(url, { protocols, headers, signal })`, a WebSocket over the kernel's transport (its `websocket`, so `fetchTransport({ webSocket: true })` in a browser), shaped like `WebSocketStream`. It resolves once the socket is open, with `url`, the `protocol` the far end chose (always one the program offered: a transport that hands back a socket with another fails the open with `EPROTO`, while browsers and Node's `WebSocket`, which `fetchTransport()` uses, fail such a handshake themselves without saying why, so the open fails with `ECONNREFUSED`), a `readable` of messages (`string` for text, `Uint8Array` for binary), a `writable` for sending them, `closed` (the `{ code, reason }` of the close, from either side, whether or not the program reads) and `close({ code, reason })`. An `http(s):` URL is taken as `ws(s):`. Sending waits while more than 1 MiB is buffered. A signal aborts the open, and after it the socket. A transport without WebSockets rejects with a `TypeError` whose `cause.code` is `ENOTSUP`. One that cannot connect rejects with the `code` its error carries (an errno, such as `EAI_NONAME`, `ECONNREFUSED`, `ETIMEDOUT` or `EPROTO`), else `ECONNREFUSED`. `ws://host.slicc.internal:port` reaches the real machine's loopback, as it does through the proxy;
552
553
  - `fetch(input, init)`, the web's `fetch` over the kernel's transport, the one the proxy uses, so the embedder's transport, its WebSocket and proxy settings and the kernel's routes apply. The response body streams as the program reads it. On a transport with `traits.streamingBodies` (below), a `ReadableStream` body (with `duplex: 'half'`, which `Request` requires) streams to it as the program produces it, with no size limit of the kernel's, and a redirect that would send it again (a 307 or 308) rejects with a `TypeError`, as the fetch standard has it. Other bodies (a string, a buffer, a `Blob`, a `FormData`) can be sent again, so they go whole even there. Otherwise the request body is read whole first, and one larger than the transport's `maxRequestBody` is answered `413`, as through the proxy. A signal aborts the request at any stage. Redirects follow the fetch rules: at most 20, a 301 or 302 after a `POST` and a 303 turn into a `GET` without a body, `Authorization` is dropped across origins, and `redirect: 'manual'` returns the redirect itself. On a transport that follows redirects on its own (`fetchTransport()` in a browser) only `'follow'` works: `'manual'` and `'error'` reject with a `TypeError` whose `cause.code` is `ENOTSUP`, since such a transport never shows the program a redirect. A status the transport chose comes back as a response whose body is the reason. An unreachable host, or no transport at all, rejects with a `TypeError` whose `cause.code` is `ECONNREFUSED` or `ENETUNREACH`, as Node's `fetch` does. A `data:` URL is answered by `fetch` itself, as the fetch standard decodes it, without a transport: for any method, a `HEAD` without a body, and its type parsed and serialized as a MIME type (lowercase, no spaces, invalid parameters dropped), as Node's `fetch` answers; any other scheme than `http:` and `https:` rejects with a `TypeError` whose `cause.code` is `EPROTONOSUPPORT`, at the start as for a redirect target. A URL on one of the fetch standard's bad ports (25, 6000 and the rest) rejects too, as Node's `fetch` does: a `TypeError` `fetch failed` whose `cause` is an `Error` `bad port` (no `code`), at the start and as a redirect target, on the kernel's own loopback too ([#822](https://github.com/ai-ecoverse/slicc-kernel/issues/822)). `redirect: 'error'` rejects with a `TypeError` whose `cause` is an `Error` `unexpected redirect`, and a body whose connection ends early fails its read with `TypeError` `terminated` whose `cause` is undici's: `name` `SocketError`, `message` `other side closed`, `code` `UND_ERR_SOCKET`. The request goes out with its headers in the order the program gave them (an object's or an array's order; a `Headers` object's is sorted), lowercase as `Headers` keeps them. The global `fetch` stays withheld;
553
554
  - **`fetch` and `websocket` to the kernel's own loopback:** a URL whose host is `127.0.0.0/8`, `0.0.0.0`, `[::1]`, `localhost` or `*.localhost` goes to the kernel's own loopback, as a guest's sockets do (`<port>.kernel.localhost` takes its port from the name). So it reaches whatever listens on that port in the kernel, the CDP facade at `SLICC_CDP_URL` and its `/json` included. It never goes to the transport: with nothing listening, it rejects with a `TypeError` whose `cause.code` is `ECONNREFUSED`, with or without a transport. The kernel speaks HTTP/1.1 and WebSocket (RFC 6455) to the listener itself, and `fetch` handles the redirects and compressed bodies it answers, as for any server. Its request head starts as undici's does, `host` then `connection`, then the program's headers in their order, then `content-length`. It sends `connection: close` where undici sends `keep-alive`, because each request is a connection of its own; a server sees no other difference. A WebSocket's `closed` settles after the closing handshake once the server has closed the connection (RFC 6455 §7.1.4), as undici's `close` event does, or at the latest 5 s after the Close frames. A refused upgrade rejects with a `TypeError` whose `cause` is an `Error` naming the status, `the server answered the upgrade with 503 Service Unavailable`, with `code` `EPROTO`. `https:` and `wss:` there reject with `ENOTSUP`. The listener sees a connection from `127.0.0.1` whose `SO_PEERCRED` is the program's pid, uid and gid, as if the program had connected itself. `host.slicc.internal` still goes to the transport as the real machine's loopback, for `fetch` too, under the transport's policy (`nodeTransport({ hostLoopback })`). A redirect from any other URL into the kernel's loopback rejects with `cause.code` `EACCES`, so a server elsewhere can't steer a program's request to the kernel's own servers; `redirect: 'manual'` still returns it;
@@ -7598,7 +7598,7 @@ function satisfies(version, range2) {
7598
7598
  }
7599
7599
 
7600
7600
  // src/version.ts
7601
- var KERNEL_VERSION = true ? "1.143.2" : "0.0.0-dev";
7601
+ var KERNEL_VERSION = true ? "1.143.4" : "0.0.0-dev";
7602
7602
  var OS_RELEASE = "6.0.0-slicc";
7603
7603
  function procVersion() {
7604
7604
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -10500,22 +10500,22 @@ function noted(watch, note) {
10500
10500
  if (watch.overflowed) return;
10501
10501
  if (watch.notes.length >= WATCH_NOTES_LIMIT) {
10502
10502
  watch.overflowed = true;
10503
- watch.notes.push({ overflow: true });
10503
+ watch.notes.push({ overflow: true, seq: note.seq });
10504
10504
  } else watch.notes.push(note);
10505
10505
  watch.wake?.();
10506
10506
  }
10507
- function route(watch, type, path) {
10508
- for (const root of watch.roots) if (routed(watch, root, type, path)) return;
10507
+ function route(watch, type, path, seq2) {
10508
+ for (const root of watch.roots) if (routed(watch, root, type, path, seq2)) return;
10509
10509
  }
10510
- function routed(watch, root, type, path) {
10510
+ function routed(watch, root, type, path, seq2) {
10511
10511
  if (path === root || type === "rename" && root !== "/" && under3(root, path)) {
10512
- noted(watch, { type, filename: watch.names.get(root) ?? baseOf3(root) });
10513
- if (type === "rename") noted(watch, { ended: true });
10512
+ noted(watch, { type, filename: watch.names.get(root) ?? baseOf3(root), seq: seq2 });
10513
+ if (type === "rename") noted(watch, { ended: true, seq: seq2 });
10514
10514
  return true;
10515
10515
  }
10516
- if (parentOf7(path) === root) noted(watch, { type, filename: baseOf3(path) });
10516
+ if (parentOf7(path) === root) noted(watch, { type, filename: baseOf3(path), seq: seq2 });
10517
10517
  else if (watch.recursive && under3(path, root)) {
10518
- noted(watch, { type, filename: path.slice(root === "/" ? 1 : root.length + 1) });
10518
+ noted(watch, { type, filename: path.slice(root === "/" ? 1 : root.length + 1), seq: seq2 });
10519
10519
  } else return false;
10520
10520
  return true;
10521
10521
  }
@@ -10529,6 +10529,7 @@ var FsWatchers = class {
10529
10529
  typed = /* @__PURE__ */ new Set();
10530
10530
  taps = [];
10531
10531
  reals = /* @__PURE__ */ new Map();
10532
+ seq = 0;
10532
10533
  reshapes = [];
10533
10534
  onReshape(listener) {
10534
10535
  this.reshapes.push(listener);
@@ -10588,7 +10589,7 @@ var FsWatchers = class {
10588
10589
  const notes = watch.notes.splice(0);
10589
10590
  watch.overflowed = false;
10590
10591
  if (watch.ended) this.typed.delete(watch);
10591
- return notes.length === 0 && watch.ended ? [{ ended: true }] : notes;
10592
+ return notes.length === 0 && watch.ended ? [{ ended: true, seq: this.seq }] : notes;
10592
10593
  }
10593
10594
  };
10594
10595
  }
@@ -10605,10 +10606,11 @@ var FsWatchers = class {
10605
10606
  if (this.typed.size === 0) return;
10606
10607
  for (const path of paths) {
10607
10608
  const at = normalizePath(path);
10608
- for (const watch of [...this.typed]) route(watch, type, at);
10609
+ const seq2 = ++this.seq;
10610
+ for (const watch of [...this.typed]) route(watch, type, at, seq2);
10609
10611
  }
10610
10612
  }
10611
- moved(from, to) {
10613
+ moved(from, to, seq2 = ++this.seq) {
10612
10614
  const source = normalizePath(from);
10613
10615
  const target = normalizePath(to);
10614
10616
  const followed2 = /* @__PURE__ */ new Set();
@@ -10624,17 +10626,25 @@ var FsWatchers = class {
10624
10626
  watch.names = names2;
10625
10627
  watch.roots = [...new Set(roots)];
10626
10628
  if (main === source) {
10627
- noted(watch, { type: "rename", filename: names2.get(target), moved: true });
10629
+ noted(watch, {
10630
+ type: "rename",
10631
+ filename: names2.get(target),
10632
+ moved: true,
10633
+ seq: seq2
10634
+ });
10628
10635
  }
10629
10636
  }
10630
10637
  return followed2;
10631
10638
  }
10632
10639
  renamed(from, to) {
10633
- const followed2 = this.moved(from, to);
10640
+ const first = ++this.seq;
10641
+ const followed2 = this.moved(from, to, first);
10634
10642
  if (this.typed.size === 0) return;
10635
- for (const path of [from, to]) {
10643
+ for (const [i, path] of [from, to].entries()) {
10636
10644
  const at = normalizePath(path);
10637
- for (const watch of [...this.typed]) if (!followed2.has(watch)) route(watch, "rename", at);
10645
+ const seq2 = i === 0 ? first : ++this.seq;
10646
+ for (const watch of [...this.typed])
10647
+ if (!followed2.has(watch)) route(watch, "rename", at, seq2);
10638
10648
  }
10639
10649
  }
10640
10650
  changed(...paths) {
@@ -10699,10 +10709,11 @@ var FsWatchers = class {
10699
10709
  const real = await realOf(at);
10700
10710
  const paths = real === at ? [at] : [at, real];
10701
10711
  this.report(paths);
10702
- if (done5) for (const type of types(fresh2)) this.emit(type, paths);
10712
+ if (done5) for (const type of types(fresh2, args)) this.emit(type, paths);
10703
10713
  }
10704
10714
  };
10705
10715
  const written2 = (fresh2) => fresh2 ? ["rename", "change"] : ["change"];
10716
+ const overwritten = (fresh2, args) => fresh2 || args[1].length === 0 ? written2(fresh2) : ["change", "change"];
10706
10717
  const reshaping = (run2) => async (...args) => {
10707
10718
  this.reshaped();
10708
10719
  try {
@@ -10712,7 +10723,7 @@ var FsWatchers = class {
10712
10723
  }
10713
10724
  };
10714
10725
  const overrides = {
10715
- writeFile: content(fs.writeFile.bind(fs), written2),
10726
+ writeFile: content(fs.writeFile.bind(fs), overwritten),
10716
10727
  ...fs.createExclusive ? { createExclusive: content(fs.createExclusive.bind(fs), () => ["rename"]) } : {},
10717
10728
  ...fs.copyFile ? {
10718
10729
  copyFile: (from, to, options) => content(
@@ -17425,7 +17436,8 @@ var Resolver = class {
17425
17436
  const abort = new AbortController();
17426
17437
  lookup2 = { work: this.lookup(key, asked, id2, abort.signal), abort, waiters: 0 };
17427
17438
  this.pending.set(id2, lookup2);
17428
- void lookup2.work.then(() => this.pending.delete(id2));
17439
+ const done5 = () => this.pending.delete(id2);
17440
+ void lookup2.work.then(done5, done5);
17429
17441
  }
17430
17442
  return waitFor(lookup2, signal);
17431
17443
  }
@@ -17443,7 +17455,7 @@ var Resolver = class {
17443
17455
  ])
17444
17456
  );
17445
17457
  } catch {
17446
- return [];
17458
+ throw new KernelError("ETIMEDOUT", `resolve ${name2}: the uplink gave no answer`);
17447
17459
  }
17448
17460
  const addresses = answer2.addresses.filter((a) => this.usable(name2, a, family));
17449
17461
  if (addresses.length > 0) this.remember(id2, addresses, answer2.ttl);
@@ -17476,12 +17488,16 @@ function waitFor(lookup2, signal) {
17476
17488
  reject(new KernelError("EINTR"));
17477
17489
  };
17478
17490
  signal?.addEventListener("abort", abort, { once: true });
17479
- void lookup2.work.then((value2) => {
17491
+ const settle3 = (finish) => {
17480
17492
  if (signal?.aborted) return;
17481
17493
  lookup2.waiters--;
17482
17494
  signal?.removeEventListener("abort", abort);
17483
- resolve2(value2);
17484
- });
17495
+ finish();
17496
+ };
17497
+ void lookup2.work.then(
17498
+ (value2) => settle3(() => resolve2(value2)),
17499
+ (err) => settle3(() => reject(err))
17500
+ );
17485
17501
  });
17486
17502
  }
17487
17503