Skip to content

chore: sync upstream Bun with paired OpenClaw WebKit - #124

Merged
steipete merged 19 commits into
mainfrom
claude/w150-upstream-webkit-sync
Oct 5, 2026
Merged

steipete merged 19 commits into
mainfrom
claude/w150-upstream-webkit-sync

Conversation

@steipete

@steipete steipete commented Oct 5, 2026 •

Copy link
Copy Markdown

This merges oven-sh/bun through 9bd19c98eacc01530a4e7609bc427abffa87d77e and adopts immutable OpenClaw WebKit 42ab38d705 in the same change. The newer engine provides upstream's idle compiler-thread and Atomics.wait memory release. Its namespace API also requires the Bun counterpart from #106, which is retained as a distinct credited prerequisite commit.

The import/require namespace change removes the former prototype/private-field workaround: imports remain unmarked, while require(esm) receives its own marker and live exports. The existing #106 implementation and regressions are preserved. Thanks @robobun for the upstream namespace work adapted in oven-sh/bun#33894 and #39888.

Extended namespace qualification exposed the existing async CJS package-context bug already addressed by #117. This branch preserves that original commit as a merge prerequisite: the file-format parser hint and package interop tag remain separate. The old baseline and first candidate both reproduce the incorrect bare default; Node preserves the exports object. The original assertions remain intact.

The original two textual conflicts preserve the fork's checksum manifest resolver and event-loop idle accounting. The scalar-only bun: package-import exception, resolver/cache fixes, module-loader context, CallSite work, macOS cache serialization and current VM cached-data fixes remain intact. The changelog retains current main as an exact prefix. The complete published manifest is committed unchanged; all nine artifact digests, sizes and target names were independently checked against the immutable release. There is no upstream-artifact fallback.

The TLS nightly update is retained as an additional merge commit, preserving the previous candidate and upstream history. Its TLS setup now runs once per connection, avoiding repeated setup when a pooled socket is reused during renegotiation. The only additional conflict joins the TLS test helpers, preserving the fork default-CA coverage. The engine target is unchanged.

Qualification is complete on head 65a7af856178f994b5319404c3602ae71d991980. The final upstream delta contains PostgreSQL/MySQL query-isolation fixes, imported unchanged in an additional merge. WebKit, HTTP and module-loader paths are unchanged by that delta. Its direct SQL proof passes all 38 tests, including real PostgreSQL and MariaDB containers:

Gate Result
Direct Linux release/no-LTO build and startup Exact final head builds and starts with the pinned published engine
Fork engine/nightly selection, including stack-position files Baseline 47/47 rows; candidate 47/47; zero regressions (45 test files plus two dependency checks)
Upstream memory-release tests 3/3 pass
Namespace and package-interop suites 52/52 pass, including the previously failing assertion
Node 24.21 resolver oracle 1,334 cases per runtime; zero normalized Node/baseline/candidate differences
New PostgreSQL/MySQL regressions 38/38 pass, including real database and wire-protocol fault controls
Full upstream fetch TLS suite Final head: 64 pass, one upstream TODO; fork main fails only the new renegotiation regression, while the prior candidate also passes 64; zero lost passes
Independent Codex review Local and complete final-branch P2 scoped-clean
Exact-head native CI Linux 226/226 rows and macOS 229/229 rows pass
OpenClaw Linux consumers 20 files, 474 passed / one expected platform skip, zero regressions; carried across the later test-only and SQL-only changes; HTTP/resolver/engine paths unchanged

The final CI merge tree matches the reviewed candidate exactly. Format, JavaScript/source lints, Clippy and Miri pass. The explicitly advisory Mordant check reports the existing resolve_target boolean-argument warning in an unchanged resolver blob.

The initial consumer matrix had zero regressions, but both Bun binaries hit the same packaged Gateway setup failure before its execution assertions. Restoring the isolated runner’s omitted Linux user-bus environment makes all 20 files pass on Node, fork main and the prior candidate with default publication checks intact. The latest canary consumer replay also passes all 20 files: 474 passed and one expected Windows-only skip, with zero regressions. Both grouped and individual Vite production builds pass on the paired-engine candidate and Node; fork main reproduces the missing zod.z export in both modes. The unchanged memory, namespace and resolver-oracle proof remains applicable to the later TLS and SQL deltas.

The expanded HTTP selection exposed one inherited fixture failure on both platforms: fetch-leak.test.ts returned before its server listened or its sampling timer completed, then disposal raised ERR_SERVER_NOT_RUNNING. The test-only repair awaits readiness and 32 body-validated requests, joins the child, and checks finite Response/AbortSignal object counts. The CI-built Darwin executable passes the repair; the original fixture reproduces the failure, and a deliberate retained-response control fails with 33 live objects. The repaired full leak file passes all 31 tests on Linux baseline and candidate and passes Linux CI. A subsequent Darwin run exposed a separate inherited TLS fixture race: its server could start renegotiation before the client observed its initial handshake. A framed client-ready message now establishes that ordering, while retaining the exact two-handshake assertion and acknowledgement-gated payload. The full 21-test file and ten focused Darwin repetitions pass; deliberately pausing again on the second handshake still fails. Both lanes passed on the preceding head, with one unchanged-head Darwin retry after a VM timeout timing observation. The full VM file passed five times on each of the verified fork-main and candidate Darwin binaries; the assertion and timeout implementations were unchanged. That failure remains recorded, not claimed fixed. Both full CI lanes pass on the final SQL-sync head.

Landing uses a merge commit, preserving upstream history. No Bun release or tag is part of this PR. Remaining inspector, accounting and stack adapter PRs stay separate. Supersedes #106 only when this PR merges.

robobun and others added 19 commits October 2, 2026 23:49
…ven-sh#43962)

### What
A TLS client that calls `end()` before its first handshake step (in the
tick of `tls.connect()`, in the next tick, or inside `'connect'`) sent a
bare FIN with no ClientHello. It never reported a certificate that it
refuses, and against a server that keeps its side open it never exited.
Node sends the ClientHello first.

This is a regression that has not shipped: oven-sh#42181 made `_final` send the
FIN at once. The script of oven-sh#17190 passes on 1.4.2 and fails on `main`.

### How
- `us_internal_ssl_on_open` runs the `'connect'` listeners and then
sends the ClientHello. `us_internal_ssl_shutdown` now holds the FIN back
in between, and `us_internal_ssl_on_open` sends it right after the
ClientHello.
- Only `tls.connect()` opts in, from a `'connect'` listener that it
registers first. That is where Node registers `_start`
([wrap.js#L1795](https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1795)).
`Bun.connect`, `upgradeTLS`, a second `connect()` of the same
`TLSSocket` and `new TLSSocket().connect()` never set the bit.

### Evidence
- `node-tls-duplex-end-verify.test.ts`: 68 pass on a debug and on a
release build. Under `node --test` (v26.7.0), 67 pass and 1 is skipped.
- Regression test for oven-sh#17190.

### Not in this PR
With the ClientHello sent, an early `end()` behaves like an `end()` one
turn after `'connect'` does on `main`. Three differences from Node that
`main` has there now show for an early `end()` too. Measured on earlier
heads of this branch:
- TLS 1.2, `rejectUnauthorized`, untrusted chain: `error
UNABLE_TO_VERIFY_LEAF_SIGNATURE`, where Node reports `end`, `error
ECONNRESET`.
- More than 10 such clients at once in one process: the ones that wait
in the low-priority handshake queue report `ECONNRESET` and never read
the server's flight.
- A TLS 1.2 session that is offered for another server name: `error
ERR_TLS_CERT_ALTNAME_INVALID`, where Node offers no session and reports
`ECONNRESET`.

Also measured on earlier heads, inside the tick in which the FIN is
held:
- `end()` and then `resetAndDestroy()`: the FIN or the reset is missing,
where `main` sends both. Node throws on `resetAndDestroy()` of a TLS
socket.
- `_handle.write()` after `_handle.shutdown()` returns 0, where `main`
returns -1.

`end()` over a Duplex, or over a wrapped socket that is still
connecting, is still lost, as on `main`.

---------

Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
…swers or throws for a body that cannot be read (oven-sh#44504)

### What does this PR do?

Fixes regressions on main from oven-sh#42053, which is in no release. Replaces
oven-sh#43916.

```js
const f = Bun.file(path);        // 5 bytes
new Response(f).clone();         // nothing reads a body
await Bun.write(path, "123456789");
f.size;                          // 1.4.2: 9, main: 5
```

After `clone()` of a body that is a `Bun.file(path)`:

| | 1.4.2 | main | this PR |
|---|---|---|---|
| file grows 5 to 9 bytes, then `f.size` | 9 | 5 | 9 |
| same, then `new Response(f).body`, or `.body` of either clone |
`123456789` | `12345` | `123456789` |
| `f.lastModified` after `utimes`, `f.exists()` after `unlink` | fresh |
stale | fresh |
| the path is a directory | both bodies reject with `EISDIR` | `clone()`
throws | both bodies reject with `EISDIR` |
| `Bun.file(fd)` of a directory, or of an fd that is not open | both
bodies reject (`EISDIR`, `EBADF`) | `clone()` throws | both bodies
reject (`EISDIR`, `EBADF`) |
| a procfs file: both bodies and `f.text()` | whole | `""` | whole |

The same holds for `Request.clone()`, `new Request(request)`, `new
Request(request, init)` and `new Request(url, response)`.

**Cause.** `clone()` has to know whether a path names something that
yields its bytes once (a pipe, a terminal), because two Blobs over that
would compete and the body is teed instead. oven-sh#42053 asks with
`resolve_file_stat`, which writes `mode`, `seekable`, `max_size` and
`last_modified` on the `Store`. The body shares that `Store` with the
`Bun.file()` it was made from, and nothing invalidates those fields, so
`clone()` became one more call that freezes them. Separately, its rule
was "tee whatever is not a regular file", and a tee over a directory
cannot start.

**Fix.** `store_yields_bytes_once` (new, `src/runtime/webcore/Blob.rs`)
answers for a Blob body, and `Value::clone_with_readable_stream` calls
it in place of `store_reads_repeatably`.
- It keeps the mode it stats in a field of its own,
`mode_seen_by_clone`, which nothing else reads. That is the fix for the
stale answers and for procfs.
- It does not count a directory as yielding bytes once, by path or by
fd, nor an fd that `fstat` fails for. There are no bytes to compete for,
so the body is duped like a regular file and each body fails when it is
read, as in 1.4.2. A failed `clone()` on main also leaves the original
body used.
- For an fd it asks with `resolve_file_stat`, which is what the tee of
an fd does next (`to_readable_stream` resolves the size). So the `fstat`
only moves earlier, and an fd body is cached as on main.
- It still stats once per `Store` and still uses a stat that `size` or
`.body` cached earlier, so `clone()` makes the same syscalls as on main.

**Kept from oven-sh#42053:** a body over an fd, `Bun.stdin`, a FIFO by path and
a terminal by path is teed, and a cloned `Bun.file().stream()` body
keeps its type.

**This is a partial fix.** The arm for an unread `.stream()` body is not
touched, so `new Response(f.stream()).clone()` still caches the stat:
`f.size` and the rest stay stale after it, and a procfs `.stream()` body
reads `""`. Taking the cache away there makes the bodies, which oven-sh#42053
turns back into Blobs, behave in ways that neither 1.4.2 nor main has.
Most of those are the right value, but `console.log` of a cloned
`slice().stream()` body would print `0.0 ZB`. That needs its own
decision, so it is left out.

**Nothing that main answers correctly after a `clone()` gets worse.**
Main has two answers right only because `clone()` writes the cache.
Taking the write away alone would bring two older bugs back for code
that clones, so they are fixed where they are:
- `f.exists()` stayed `false` for good once a stat had failed, with or
without a `clone()` (`await f.exists()`, create the file, `await
f.exists()`). `get_exists_sync` now stats again while no stat has
succeeded.
- `console.log` of a `Response` or `Request` over `Bun.file(p).slice(1,
4)` prints `0.0 ZB` until something stats the file, and on main
`clone()` was that something. `get_size_for_bindings` now also accepts
the mode `clone()` saw, so a cloned slice prints `3 bytes` as on main.
- `0.0 ZB` is `u64::MAX`, "unknown", run through the byte formatter. It
is no longer printed: `Response {` and `Request {` carry no size when it
is unknown. This is what makes the label right for a file that was
deleted after the clone, where main prints the size it had.
- Printing such a body left the 0 of the failed stat in it. The next
`console.log` would then have printed `0 KB`, and `text()` read `""`
even after the file was created, with or without a `clone()`, on 1.4.2
too. `get_size_for_bindings` no longer keeps that 0.

A slice that was never cloned still has no size that `console.log` can
print. It printed `0.0 ZB` and now prints none. An empty file and a
procfs file give the same `stat`, so cutting the slice to `st_size` gets
one of them wrong, and I left that alone.

**Why not oven-sh#43916.** It drops the stat for a Blob body, so a FIFO or a
terminal given by path as the Blob is duped again and one body gets
nothing. It stats uncached for a `.stream()` body, which costs 1000
`newfstatat` per 1000 clones of streams from one shared `Bun.file()`,
where main pays 1. And it has the new behaviours described under
"partial fix".

### How did you verify your code works?

Tests, each run on a debug build of main, on this branch and on 1.4.2:

| | main | here | 1.4.2 |
|---|---|---|---|
| `body-clone.test.ts`, 12 regression tests: `size` and a later `.body`
through each of the five ways to clone, `lastModified` and `exists()`,
both bodies after the file grew, the size `console.log` prints, a procfs
file, a directory by path and by fd, an fd that is not open | fail |
pass | 11 pass, the printed size fails |
| `body-clone.test.ts`, 3 tests that pin what main has right: a FIFO
given by path as the Blob itself is teed, so is a regular file given by
fd, and `exists()` sees a file that was created before the clone | pass
| pass | fail |
| `bun-file-exists.test.js`, 2 tests: one `Bun.file()` sees a file that
is created after `exists()` or `size` found none | fail | pass | fail |
| `inspect.test.js`, 2 tests: no size is printed for a missing file or a
directory, and printing a body whose file does not exist yet leaves it
as it is | fail | pass | fail |

With the `is_fd` clause of `store_yields_bytes_once` disabled, the fd
test is the only one in `body-clone.test.ts` that fails.

A differential run on Linux x64 of this branch against a debug build of
main at the same base. It crosses 12 kinds of body (regular, empty,
large, symlink, missing, directory, `/dev/null`, procfs, fd of a file,
fd of a directory, fd that is not open, in-memory Blob), 4 earlier reads
of the handle, 6 body forms, the 5 ways to clone plus 2 controls that do
not clone, up to 12 changes to the file before or after the clone, and
29 observations, each in a fresh directory with a fresh handle. Two of
them do a thing twice: print twice, and print and then read.

The rule it checks: **a row that differs from main has to be correct**,
not merely what an older version did. Each scenario records what is on
disk at that moment (existence, size, mtime, content). A changed row has
to match that, or, where no such truth can be derived, what the same
scenario answers without a `clone()`. A row where main matches the truth
and this branch does not is reported as a regression on its own. There
is none.

| of 187,404 rows | |
|---|---|
| same as main | 183,155 |
| differs, matches what is on disk | 3,108 |
| differs, same as without a `clone()` | 775 |
| differs, prints no size where main prints `0.0 ZB` and the size could
be known | 342 |
| differs, wrong | 0 |
| a `/dev/null` body served with `Bun.serve`, which flips between a
reset and a 500 page on main between its own runs | 2 |
| not an observation of `clone()`: `fetch()` refuses a GET `Request`
with a body, for every kind of body on both builds | 22 |

Extra syscalls for 1000 clones (`strace`, N=1000 minus N=0),
`newfstatat` / `fstat` / `openat`:

| | before oven-sh#42053 | main | this PR |
|---|---|---|---|
| `new Response(Bun.file(p))` | 0/0/0 | 1000/0/0 | 1000/0/0 |
| one shared `Bun.file()` | 0/0/0 | 1/0/0 | 1/0/0 |
| `Bun.file(p).stream()` body | 0/1000/1000 | 1000/0/0 | 1000/0/0 |
| `shared.stream()` body | 0/1000/1000 | 1/0/0 | 1/0/0 |
| `Bun.file(fd)` body | not measured | 0/2000/1 | 0/2000/1 |
| one shared `Bun.file(fd)` | not measured | 0/1001/1 | 0/1001/1 |

Unchanged from main: 48 cases over a FIFO, `/dev/stdin`, `Bun.stdin` and
a file replaced by a FIFO (3 runs each), `/dev/tty` and `/dev/stdin`
under a pty, and `/dev/null` and `/dev/zero` slices.

Suites on the debug build, no failures: `body-clone`, `body`,
`response`, `blob`, `blob-write`, `body-stream`, `request`, `FormData`,
`bun-file`, `bun-file-exists`, `inspect`, `console-log`,
`bun-serve-file`, `bun-serve-static`. In `fetch.test.ts` and
`bun-write.test.js` one test each fails with `EFBIG`, here and on main,
because I ran them under a 200 MB file size limit. `bun run
rust:check-all` passes for `x86_64-pc-windows-msvc` and
`aarch64-apple-darwin`.

Not verified: Windows and macOS at runtime. The two fd tests are skipped
on Windows.
…n-sh#44519)

Fixes oven-sh#44075. Closes oven-sh#44449. On macOS, dns.lookup(), fetch() and sockets
now resolve split-DNS names, for example a VPN resolver behind iCloud
Private Relay. getaddrinfo(3) resolves these names, but Bun reported
ENOTFOUND. mDNSResponder asks the next resolver after a negative answer
only when the client sends the failover attribute.
DNSServiceGetAddrInfoEx (oven-sh#40675) accepts that attribute but never sends
it, and the daemon reads it only for QueryRecord requests. This PR does
what libinfo does: one DNSServiceQueryRecordWithAttribute per family.
macOS 12 falls back to plain DNSServiceQueryRecord on the same code
path. Thanks to @mimi1vx for the diagnosis. Compared with oven-sh#44449: the
hostname is sent as written (a trailing dot turns off search domains, so
http://intranet/ would stop resolving), a link-local AAAA keeps its
interface scope (fe80::1%7), and removed records are still ignored.
Verified: new tests put a scripted mDNSResponder behind the unix socket
of the daemon and check the requests and the answers. All 10 pass with
bun bd test test/js/bun/dns/. The 9 that existed at that time all fail
on 1.4.3 with USE_SYSTEM_BUN=1, and the tenth was not run there. The
requests are now identical to those of getaddrinfo(3), and 21 lookups
through the real daemon match 1.4.3. Not verified: real failover, which
needs a split-DNS machine. @mimi1vx @sassdavid, can you try this build
with bunx bun-pr?
…ods (oven-sh#43452)

### Problem
- The per-encoding write methods of `Buffer` (`utf8Write`,
`latin1Write`, `asciiWrite`, `hexWrite`, `base64Write`,
`base64urlWrite`, `ucs2Write`, `utf16leWrite`) accept a value that is
not a string. `Buffer.alloc(4).utf8Write(123)` returns `3`, and an
object value's `toString()` runs. Node throws `TypeError
[ERR_INVALID_ARG_TYPE]: argument must be a string`.
- The cause: both host function templates start with `toStringOrNull()`
on the value (`src/jsc/bindings/JSBuffer.cpp:2453`, `:2487`).
- No user reported this. A comparison with Node found it during work on
oven-sh#30138.

### Fix
- The new helper `stringArgumentOrThrow` checks `isString()` and throws
Node's error. Both templates call it in place of `toStringOrNull()`.
- The order follows Node. `utf8Write`, `latin1Write` and `asciiWrite`
check the bounds first, so `ERR_BUFFER_OUT_OF_BOUNDS` wins. The others
reject the value before they read `offset` or `length`.
- `Buffer.prototype.write` is not changed. oven-sh#30138 changes its value
check (oven-sh#43451) and adds the identical helper block, so the two PRs merge
in either order.
- Verified: `test/js/node/buffer.test.js`, block `with a value that is
not a string`: 35 tests fail on canary 1.4.3 and pass here. Also the
rest of that file and the vendored `test-buffer-*.js` files.

### Background
- In Node these methods are C++ (`StringWrite` and `SlowWriteString` in
`src/node_buffer.cc`). Both start with `THROW_AND_RETURN_IF_NOT_STRING`:
a fixed message, no conversion of the value.
- Node wraps `utf8Write`, `latin1Write` and `asciiWrite` in JS that
checks the bounds before the C++ code runs. That gives the two orders.
- `toStringOrNull()` is JSC's `ToString`. For an object it runs user
code, which can detach or resize the buffer.

<details><summary>Notes</summary>

**Behavior change.** Code that passed a non-string value to these
methods got the value's string form written. It now gets
`ERR_INVALID_ARG_TYPE`, as in Node. The string path does not change:
`toStringOrNull()` returned the same `JSString` for a string.

**Probe, Node v26.3.0 against canary 1.4.3 (367d939).** `buf` is
`Buffer.alloc(8)`. `A` is `ERR_INVALID_ARG_TYPE` "argument must be a
string". `B` is `ERR_BUFFER_OUT_OF_BOUNDS`.

| call | Node | canary | this PR |
| --- | --- | --- | --- |
| `utf8Write(123)` | A | `3` | A |
| `utf8Write()` | A | `8` (writes `"undefine"`, the start of
`"undefined"`) | A |
| `utf8Write(Symbol())` | A | `TypeError`, no code | A |
| `utf8Write(new String("ab"))` | A | `2` | A |
| `hexWrite(123)` | A | `1` | A |
| `base64Write(null)` | A | `3` | A |
| `ucs2Write({})` | A | `8` | A |
| `utf8Write(spy)`, `hexWrite(spy)` | A, no call into `spy` |
`Symbol.toPrimitive` runs | A, no call |
| `utf8Write(123, -1)`, `utf8Write(123, 0, 9)` | B | B | B |
| `utf8Write(123, 8)`, `utf8Write(123, 0, 0)`, `utf8Write(123, NaN)` | A
| `0` | A |
| `utf8Write(123, offsetObject, lengthObject)` | A, after both `valueOf`
calls | `1` | A, after both `valueOf` calls |
| `hexWrite(123, -1)` | A | `ERR_OUT_OF_RANGE` | A |
| `hexWrite(123, 9)` | A | B | A |
| `hexWrite(123, offsetObject, lengthObject)` | A, no `valueOf` call |
`1` | A, no `valueOf` call |
| `utf8Write(123)` on an empty or a detached buffer | A | `0` | A |

`latin1Write` and `asciiWrite` behave like `utf8Write`. `base64Write`,
`base64urlWrite`, `ucs2Write` and `utf16leWrite` behave like `hexWrite`.
After the fix, the 293-line probe output is identical to Node's for
these methods, except for a receiver that is not a Buffer (see below).

**The new tests against Node.** The block uses `bun:test`, like the
blocks around it. I ran the same block under Node v26.3.0 with a small
shim for `describe`, `it.each` and `expect`. 31 of 31 tests pass there.
The other 4 are for `utf16leWrite`, which Node does not have. It shares
the host function of `ucs2Write`. Upstream Node has no test that asserts
this message for `Buffer`, so there is no upstream file to port.

**No user code runs.** The test passes a Proxy whose handler is also a
Proxy, so a lookup of any trap is recorded. The canary records two `get`
traps and writes 1 byte. Node and this PR record nothing. This also
guards against a later switch to `Bun::V::validateString`, which reads
`constructor.name` from the value to build its message (oven-sh#43451).

**The whole class.** Node uses `THROW_AND_RETURN_IF_NOT_STRING` at four
places in `src/node_buffer.cc`. Two are `StringWrite` and
`SlowWriteString`, which this PR covers. The other two are `Btoa` and
`Atob`. User code cannot reach them with a non-string: `btoa(123)`
returns `"MTIz"` in Node and in Bun, because the JS layer converts the
value first.

**The `write()` sites this PR leaves**, in
`jsBufferPrototypeFunction_writeBody`, with line numbers on this branch:
- `JSBuffer.cpp:2565`, the `buf.write(value, encoding)` form: a bare
`toString()`, so `buf.write(123, "utf8")` returns `3`.
- `JSBuffer.cpp:2554` and `JSBuffer.cpp:2596`: `Bun::V::validateString`
throws the right code with a different message, and `:2596` runs before
`parseEncoding` (`:2605`), so `buf.write(123, 0, "bogus")` throws
`ERR_INVALID_ARG_TYPE` where Node throws `ERR_UNKNOWN_ENCODING`.

oven-sh#30138 owns all three, and its description links oven-sh#43451. At head
`2edc3da077` its `writeBody` calls `stringArgumentOrThrow` at each site,
after it resolves the encoding. I read that in its source. I did not
build that branch.

**Merge checks** with `git merge-tree --write-tree`, this branch at
`5cf21732dd`:
- Clean with `main` (`519963edc8`), with oven-sh#30138 (`2edc3da077`) and with
oven-sh#43368 (`6cd01edf50`).
- With oven-sh#30138 in either order, the merged `JSBuffer.cpp` has one
definition of `stringArgumentOrThrow` and five call sites. The helper
block, with its comment, is byte-identical in the two PRs.
- oven-sh#43470 (`8e6c5aa3d9`) conflicts with this branch on the comment above
the helper, because it has the earlier three-line comment. It conflicts
with oven-sh#30138 on the same lines.

**Comments.** The last commit (`5cf21732dd`) shortens three comments and
changes no code. The comment above `stringArgumentOrThrow` now has the
text that oven-sh#30138 uses, so that git merges the two identical blocks
without a conflict.

**Deleted guard.** In
`jsBufferPrototypeFunction_StringWriteWithEncoding` the detach check was
`(strValue.isObject() || offsetValue.isObject()) && isDetached()`. The
value is now always a string at that point, so `strValue.isObject()` is
gone.

**Not changed: a receiver that is not a Buffer.**
`Buffer.prototype.hexWrite.call({}, "ab")` throws a `TypeError` with no
code (`Expected ArrayBufferView`). Node throws `ERR_INVALID_ARG_TYPE`
"argument must be a buffer". The `<enc>Slice` methods have the same
difference, so it is a separate change. oven-sh#44525 tracks it.

**Not changed: the conversion of `length` runs before the offset
check.** With an `offset` that is out of bounds, `utf8Write`,
`latin1Write` and `asciiWrite` call the `valueOf` of an object `length`
before they throw `ERR_BUFFER_OUT_OF_BOUNDS`. Node throws for the offset
first. The order is the same on `main`. A review of this PR found it.
oven-sh#44526 tracks it.

**Exception scopes.** The new tests pass under
`BUN_JSC_validateExceptionChecks=1 BUN_JSC_dumpSimulatedThrows=1` with
the debug build. Every one of them takes the throw path.

**Suites run with the debug build (ASAN).**

On this branch at `5cf21732dd`, which includes `main` at `519963edc8`:
- `test/js/node/buffer.test.js`: 718 pass, 1 skip, 0 fail, run with
`--timeout 120000`. The file runs its `write()` tests a second time
through a JS `write` that calls `utf8Write` and the other methods.
- My machine was heavily loaded by other work during that run. With the
default 5 s limit, 18 tests of this file failed in one run, all by
timeout, and the 35 new tests passed. `main` at the same commit passed
683 of 684 with the default limit, in 78 s, a few hours earlier.
- The 35 new tests under `BUN_JSC_validateExceptionChecks=1`: 35 pass.
- `test-buffer-write.js` and `test-buffer-write-fast.js` in
`test/js/node/test/parallel/`: exit 0.
- The 293-line probe: the output differs from Node's only in the
receiver lines and the `write()` lines.

At the first commit of this PR (`2007e66a6e`), which has the same code
before the comment change and the merge of `main`:
- `test/js/node/test/parallel/test-buffer-*.js`: 62 of 63 files exit 0
when run directly. The other one, `test-buffer-resizable.js`, uses
`node:test` and needs the test runner. It passes there (2 pass).
- `buffer-compare-bounds`, `buffer-concat`, `buffer-copy-fill-detach`,
`buffer-from-symbol-to-primitive`, `buffer-indexOf-detach`,
`buffer-utf16`, `buffer-inspectmaxbytes`, `buffer-resolveObjectURL`
(`test/js/node/*.test.ts`): all pass.
- `test/js/node/buffer-jit.test.ts`: 11 pass, 1 fail. `differential
fuzzer` exceeds its 120 s limit on my machine, a debug build with few
cores. It exceeds the limit in the same way with `src/` at
`origin/main`, and the file calls none of the methods in this PR. I saw
this only locally, not in CI.

**Self-review.** The review asked for three things in this description:
name the `write()` sites that this PR leaves, name an owner for them,
and say that no user reported the bug. All three are done. It also
weighed a fix for `buf.write(value, encoding)` in this PR and advised
against it, because oven-sh#30138 and oven-sh#43368 both change that line and already
conflict with each other on it. The last commit changes comment lines
only, so it had no new review.

</details>
…Bun.sleepSync (oven-sh#44560)

Bumps mimalloc to oven-sh/mimalloc#59.

- mi_heap_new + mi_heap_destroy (every MimallocArena) is about 7x cheaper with one allocation and 13x cheaper when unused, and no longer slows down when several threads create heaps at once.
- Purging stops doing work that is undone right away: an allocation takes resident memory before purged memory, purge_delay is a minimum age while threads are busy, and a park no longer wakes the scavenger every time.
- Memory that other threads free into the pages of a thread that stays blocked is given back after 30 s instead of when that thread wakes up.
- The event loop only hands its heaps to the scavenger on a poll that can block.
- Bun.sleepSync hands them over as well, so what the thread freed does not stay resident for the whole sleep.
…ory (oven-sh#44564)

Bumps WebKit to oven-sh/WebKit#768. Nothing else is in the range.

- JSC's worker threads (wasm and JIT compilers, GC helpers) release the free memory of their mimalloc heap after 100 ms idle instead of when they exit after 10 s. Fixes oven-sh#41438.
- A thread that blocks in Atomics.wait for over 100 ms does the same. It used to keep everything it had freed for the whole wait.

The wasm test is from oven-sh#41449.
Keep the parser format hint separate from the package-scope source tag. Adapts oven-sh#40940 for the async runtime loader.
Bumps mimalloc to oven-sh/mimalloc#60, which merges upstream v3.5.3 into the fork (it was at v3.5.2).

- Fixes a crash when the first mimalloc call on a new thread is mi_realloc(NULL, n), mi_reallocf, mi_strdup, mi_mallocn or mi_new, with a TLS model where the thread local heap of a new thread starts out as NULL (macOS and Windows).
- Fixes mi_free_size_aligned of a small block with an alignment above its size. Bun's build (MI_FREE_USE_PAGEMAP=1) was not affected.
- No function is removed from mimalloc.h, and no signature that src/mimalloc_sys binds changes. Instruction counts for the allocation and free paths are the same before and after.
…for it

Every Dockerfile fetched the key that signs SHASUMS256.txt from
keys.openpgp.org at build time, falling back to keyserver.ubuntu.com.
keys.openpgp.org is resetting connections ("keyserver receive failed: End
of file"), and keyserver.ubuntu.com has never had this key ("No data"), so
every image build fails. That fails the four test-docker-build-*.ts tests
on every linux x64 lane and the "Release to Dockerhub" jobs.

The public key is now written into the Dockerfile, so verifying a release
needs nothing but github.com, which the build already downloads from.

The fingerprint stays pinned: --recv-keys only accepted a key with the
fingerprint it asked for, and --assert-signer now requires the signature
to be made by it, whatever else is in the keyring.

--assert-signer fails when a file named like the signature without ".asc"
exists, which gpg's own --output created, so the verified checksums are
written to SHASUMS256.verified.txt. That also removes the "file
'SHASUMS256.txt' was NOT verified!" warning every build printed.

dirmngr was only there to reach keyservers.
…44507)

### Problem
- A TLS 1.2 server can kill a bun process that calls `fetch()` twice. It
renegotiates on the idle keep-alive connection, and the next request
that takes the pooled socket dies: `panic: abort() called`, stack
`SSL_set_session` <- `session_cache::install` <- `HTTPClient::on_open`.
Regression in 1.4.0.
- `on_open` (`src/http/lib.rs`) runs per request. Its TLS setup was
guarded by `SSL_is_init_finished(ssl) == 0`, also true during a
renegotiation, so it offered the cached session again. BoringSSL aborts
on that.

### Fix
- The TLS setup (SNI, ALPN, inline reject, session offer) becomes
`configure_tls`. Only `on_connect` calls it, the open callback of a new
connection. The guard is gone.
- Correct because uSockets starts the handshake after that callback
returns, and only a new connection takes that path.
- Verified: `test/js/web/fetch/fetch.tls.test.ts` (new test, fails on
1.4.3-canary). Also ran the fetch-keepalive, fetch-session, proxy and
tls renegotiation suites.

### Background
- fetch parks idle connections in a pool and caches one TLS session per
origin (oven-sh#36598). `SSL_set_session` offers it, legal only before a
handshake starts.
- A renegotiation is a second handshake on a live TLS 1.2 connection,
started by the server.
- Considered a guard in `session_cache::install` and a check at pool
pickup. Both leave per-connection work on the per-request path.

### Downsides
- A request that takes a socket mid-renegotiation still stalls until its
timeout: uSockets does not retry the parked write (oven-sh#37094). A `todo`
test records it.
- Cost, release builds: a new TLS connection goes from 1,427 to 1,415
instructions, a pooled request from 5,146 to 5,123. Text grows by 1,024
bytes. The stripped binary keeps its size.

<details><summary>Notes</summary>

**How to reach it.** No user code is needed beyond two `fetch()` calls
to one HTTPS origin with verification on (the default). The server
decides the rest. It does not have to withhold anything: a server that
answers and then calls `renegotiate()` is enough when the second request
starts inside the renegotiation. A server that never finishes the
renegotiation keeps the window open for as long as it likes.

**Why the guard was wrong.** `SSL_in_init()` is true whenever a
handshake object exists and is not finalized. That covers the time
before the first handshake step and the whole of a renegotiation.
uSockets allows a client renegotiation (`ssl_renegotiate_explicit`) and
runs it from its `SSL_read` loop, so the pool's idle data handler never
sees the HelloRequest and the socket stays parked while it renegotiates.

**What else the old block did to a renegotiating pooled socket.** It set
SNI and ALPN again. `session_cache::install` took the cached session out
of the cache before it offered it, so the next new connection could not
resume. It replaced the armed session sink with an unarmed one. The new
test checks the second point: after the pooled pickup, a new connection
still resumes the cached session.

**The test.** `fetch.tls.renegotiation-peer-fixture.mjs` runs under
Node, because BoringSSL cannot send a HelloRequest. It is a TLS 1.2
keep-alive origin behind a TCP relay that can hold the client's bytes,
plus a plain HTTP control server. The client fixture parks a connection,
asks the origin to renegotiate, and gets the answer only when the relay
holds the client's renegotiation ClientHello. Then it starts the request
that takes the pooled socket. A control request queued behind it on the
HTTP thread proves that the pickup ran. The test uses no timer.

**Measured, linux x64.**
- 1.4.3-canary.1 (`367d939d9`), release: the new test fails. The client
prints `panic: abort() called` at the pooled pickup.
- Debug build with ASAN of main `468efacace` without the change: the
client exits with SIGABRT at the pooled pickup.
- This branch, debug build with ASAN: the new test passes.
`fetch.tls.test.ts` (62 pass, 1 todo), `fetch-keepalive.test.ts` (49),
`fetch-session.test.ts` (34), `test/js/bun/http/proxy.test.ts` (97) and
`test/js/node/tls/renegotiation.test.ts` (21) pass. With `--todo`, the
todo test times out, as expected. `fetch-keepalive.test.ts` needed a
second run with a longer per-test timeout: four tests hit the 5 s limit
on the loaded build machine.
- Without a relay (the origin answers, then renegotiates, and the client
waits 1, 2 or 3 ms and fetches again), release builds, 5 runs per wait:
main `80de08a1db` aborts in 13 of 15 runs. This branch aborts in 0 of
15.

**What stays.** In 12 of those 15 runs on this branch the second request
does not get an answer. The server reports that the renegotiation ended,
and the request ends in a timeout. (Of the other 3, one got its answer
and two failed with `EPROTO`, which main also shows in this cell.) The
request write parks while the handshake runs, and uSockets dispatches
the writable event only in its `HANDSHAKE_COMPLETED` state, which a
renegotiation leaves until application data arrives. That is a separate
defect in `packages/bun-usockets/src/crypto/openssl.c`, and oven-sh#37094 has a
fix for it. It does not come from the session cache: it also occurs with
`BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE=1`. The todo test next
to the new test awaits that request, so it flips when the write is
retried.

**Cost numbers.** Release builds of main `80de08a1db` and of this
branch's source on the same commit.
- Instructions from entry to return, nested calls included, counted with
gdb single-steps. 3 runs, 3 consecutive calls per run.
- Open callback of a new TLS connection
(`Trampolines<uws_handlers::HTTPClient<true>>::on_open`): 1,427 on main,
1,415 here. All 9 calls of each build agree.
- `HTTPContext<true>::connect` for a pooled TLS request, sixth request
of the process: 5,146, 5,160, 5,146 on main and 5,123, 5,118, 5,123
here. Later requests vary more from run to run: 6,617 to 6,762 on main,
6,585 to 6,723 here.
- `session_cache::install` calls: 1 per 1,000 keep-alive requests and
100 per 100 new connections, on both builds.
- Size: `bun-profile` text is 80,727,274 bytes on main and 80,728,298
here (+1,024). The stripped `bun` is 80,868,936 bytes on both. Symbols:
`HTTPClient::on_open::<true>` 1,474 to 280 bytes,
`HTTPContext<true>::connect` 5,283 to 5,798, the open callback 375 to
1,582. The TLS block is now part of the open callback, and the compiler
inlines the smaller `on_open` at the three pool reuse sites.
- Tools: gdb 16.3, `llvm-nm`, `llvm-size`. `perf`, `valgrind` and
`bloaty` are not installed on the machine.

**Related.** oven-sh#41671 covers the other way into the same BoringSSL abort,
`socket.setSession()` after the handshake started. This change does not
depend on it.

**Other designs.**
- A guard in `session_cache::install`: it stops the abort, but the
cached session is still taken out of the cache and the sink is still
replaced.
- A stricter check in `on_open`: BoringSSL has no query for "the
handshake has not started", so it needs a uSockets accessor or new
per-socket state, and one branch stays on every pooled request.
- Skip a socket that is mid-handshake at pool pickup: the abort stops
only because `on_open` is not reached with such a socket. It also
changes which requests reuse a connection.
- Pass SNI, ALPN and the session to uSockets at connect time: a larger
change across every TLS client. It is not needed to remove this defect.

**Not run locally.** Windows and macOS.

</details>
Carry #106 as the source prerequisite for the rebased OpenClaw
WebKit release. Keep import namespaces unmarked and give require(esm) its
own live-export facade; preserve forwarded native namespaces and cached
require markers. Remove the former prototype/private-field workaround.

Preserve the work from ea461c8,
fba679a and
72f230b. Adapts upstream Bun oven-sh#33894 and
oven-sh#39888 by robobun; original attribution and regression coverage are retained.
This prerequisite lands together with the engine manifest and upstream sync.
Merge oven-sh/bun through c7b06d9 while preserving fork resolver, module-loader, stack, worker, VM and idle-accounting changes. Consume immutable OpenClaw WebKit 42ab38d705d4838748ccee77e7deb0e4e35515ee by checksum, paired with the distinct namespace prerequisite from #106. Retain the scalar-only bun: package-import exception and append-only changelog history.
Preserve the original commit from #117 and its contributor credit. Extended namespace qualification exposed the existing async CommonJS package-tag failure on both fork main and the first sync candidate. Node keeps the exports object for a type:module package; keep parser format and package interop metadata separate rather than weakening that assertion.
Import oven-sh#44507 as an additional merge, retaining both histories. Keep the published OpenClaw WebKit pin and fork default-CA regression coverage unchanged.
… and write nothing past a statement being parsed (oven-sh#44235)

### Problem
- `Bun.SQL` (postgres): after a pipelined query fails, two queries
issued together on that connection resolve with each other's rows. Equal
column types: no error. Different column types:
`ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE` or wrong values.
- The `ErrorResponse` arm of `PostgresSQLConnection::on` released the
failed request before its `ReadyForQuery`. A statement issued in that
gap wrote its Parse at once. The late `ReadyForQuery` then made
`advance()` write the next request's Bind first.
- Behind pgdog 0.1.60, a second `ReadyForQuery` for one Sync does the
same on every run.

### Fix
- The `ErrorResponse` arm rejects through `reject_in_flight`
(`on_undecodable_row` of oven-sh#43187, renamed). The promise rejects at once.
The request keeps its counter and the head of the queue until its
`ReadyForQuery`.
- `advance()` stops at a request whose statement is being parsed, as in
oven-sh#20986, and a debug assertion states that rule. A reply carries no
request id: it goes to the head of the queue.
- Verified: `test/js/sql/postgres-prepared-pipeline-reorder.test.ts` (24
new tests, 19 fail on main), `sql.test.ts`, every
`test/js/sql/postgres-*.test.ts`.
- Self-reviewed: 7 concerns, 6 addressed, 1 in part (Notes).

### Background
- Bun writes several prepared queries before the first reply. The server
answers each Sync with one `ReadyForQuery`, also after an error.
- `advance()` walks the request queue. It pops finished requests and
writes queued ones.
- Considered a count of Syncs: two counter updates per query, no result
changes. Considered only the clear of `IS_READY_FOR_QUERY` from oven-sh#35120:
the step-over in `advance()` stays.

### Downsides
- A new statement or simple query issued between an `ErrorResponse` and
its `ReadyForQuery` waits for that `ReadyForQuery`.
- Release binary: `.text` +256 B.

<details><summary>Notes</summary>

**Reproduction.** One connection (`max: 1`, also `sql.reserve()` and a
pool connection that carries more than one query), a real PostgreSQL,
default options:

```js
const sql = new SQL({ url, max: 1 });
const ofBob = () => sql`select owner, secret from swap_b where id = ${1}`;
const check = n => sql`select 10 / ${n}::int as q`;
await ofBob();
await check(1);
await Promise.all([ofBob(), check(0)]).catch(() => {});
const [a, b] = await Promise.all([sql`select owner, secret from swap_a where id = ${1}`, ofBob()]);
// a is the row of swap_b, b is the row of swap_a
```

The `ErrorResponse` of `check(0)` and its `ReadyForQuery` must arrive in
two reads. On loopback that happens in 35 to 86 of 100 rounds. A proxy
that forwards every byte and holds the `ReadyForQuery` until the two
reads are issued makes it 50 of 50. The tests use that proxy
(`pgHoldingProxy` in `test/js/sql/wire-frames.ts`), with no timer.

Frames after the rejection, main: `Parse(swap_a) Sync`, the held
`ReadyForQuery`, `Bind(swap_b) Sync`, `Bind(swap_a) Sync`. The queue
order is swap_a, swap_b. This branch: the held `ReadyForQuery`,
`Parse(swap_a) Sync`, `Bind(swap_a) Sync`, `Bind(swap_b) Sync`.

The queries in the gap do not have to come from the rejection handler.
Issued by a timer that was armed before the failing query, through the
same proxy, they are wrong in 10 of 10 rounds on 1.4.2 and in 0 of 10
here.

**pgdog.** pgdog 0.1.60 with its query parser on (one primary and one
replica entry for the same PostgreSQL 17) rejects a statement with a
syntax error itself. It answers the batch `Parse Describe Bind Execute
Flush Sync` with `ErrorResponse ReadyForQuery`, and then with one more
`ReadyForQuery`. The second one arrives when the Parse of the next
request is on the wire.

```js
const bad = sql`selec 2 as x`; // rejected by pgdog
const a = sql`select 'alice' as owner, ${1}::int as id`; // new to the connection
const b = sql`select 'bob' as owner, ${1}::int as id`; // prepared
await Promise.allSettled([bad, a, b]);
```

Official 1.4.2: `a` resolves with bob and `b` with alice in 50 of 50
rounds, with no proxy and no mock. This branch: 0 of 50. After the two
`ReadyForQuery`, 1.4.2 writes `Parse(alice) Describe Sync`, `Bind(bob)`,
and later `Bind(alice)`. This branch writes `Parse(alice) Describe
Sync`, and after its reply `Bind(alice)`, `Bind(bob)`. The stop in
`advance()` is the half that fixes this case.

**Why the flag was wrong.** Every `ReadyForQuery` sets
`IS_READY_FOR_QUERY`, whichever Sync it answers. With two pipelined
queries in flight, the first `ReadyForQuery` sets it while the second
Sync is not answered. `pipelined_requests > 0` is the guard for that
state. The `ErrorResponse` arm called `finish_request`, so the count
went to 0 with a `ReadyForQuery` owed. A query that fails alone twice in
a row reaches the same state: the second one is written behind the first
in the gap of the first.

**Tests.** `postgres-prepared-pipeline-reorder.test.ts` has 28 tests, 24
of them new.
- Real server through the proxy, 17 tests that fail on main: 8 shapes of
queries issued in the gap (a new statement and a prepared one, with
equal and with different column types, through `execute()`, with a
simple query, two new statements, three queries), each after a query
that failed behind another one and after a query that failed alone twice
in a row. One more on a connection reserved from a pool of 10. The shape
with two new statements does not settle on main.
- Real server, 5 tests that pass on main too: a transaction that begins
in the gap, and 4 other kinds of failing query (a simple query, a Parse
that fails with and without a parameter, a first execution without
parameters). They pin that each kind leaves the queue at its
`ReadyForQuery` and that the connection continues.
- Mock, 2 tests that fail on main. PostgreSQL sends nothing but the
`ReadyForQuery` after an `ErrorResponse`, and one `ReadyForQuery` for
each Sync. With such a server, each half of the fix alone gives every
query its own rows. So each half has a test with a peer that sends more.
- The hold: the mock sends a row after the `ErrorResponse`. With only
the hold reverted, 1 of 28 fails: the row goes to the query issued in
the gap. No real peer that does this is known.
- The stop: the mock replays the frames of pgdog. With only the stop
reverted, 1 of 28 fails, and a debug build stops at the assertion
`advance() passed a request that is not written yet`.

**Measurements.** Base is main `a4f1429148`. PostgreSQL 17.11 on
loopback, linux x64.
- Edited arms on the success path: 0 executions in 1,703 fulfilled
queries over 7 workloads (gdb counting breakpoints, debug build. Base 0
in 1,703). The calls of `advance()` from the `ReadyForQuery` arm and the
Binds written at enqueue are equal on base and branch for the 6
workloads with a fixed order. The workload with a pool of 10 differs
from run to run on both.
- Steady state, 200 queries in bursts of 10: socket writes 20 (base 20),
`advance()` entries 200 (base 200), reads 23 to 25 (base 23).
- Instructions for each pipelined query: not measured. `perf` and
`valgrind` are not in the container and have no package there. In place
of that, gdb `stepi` on the release build with symbols, 20 calls in the
middle of a burst: `advance()` 108 instructions for each call (base
108), `finish_request` 17 (base 17).
- Release binary: `.text` +256 B. `advance()` +357 B, `on_result` +29 B,
the two instances of `on()` -22 B each (`size`, `nm -S`). The debug
assertion and the comments came after this measurement. They change no
byte: `.text` and these symbol sizes are equal at `28aeaddf7` and
`21c547a52`.
- `size_of` `PostgresSQLQuery` 64 B (was 64), `PostgresSQLConnection`
672 B (was 672), `Flags` 7 B (was 7).
- For each failing query: `finish_request` at the `ErrorResponse` 0 (was
1), at the `ReadyForQuery` 1 (was 0). The shape of the report:
`advance()` entries 15 (was 15), socket writes 11 (was 12).
- Counters when the connection closes, after 7 workloads with failing
queries (50 connections): `pipelined_requests` 0,
`nonpipelinable_requests` 0, `pending_requests` 0.
- 12 shapes of queries in the gap, with the wire recorded: 0 wrong (was
6). Shapes that write before the held `ReadyForQuery`: 2 (was 12). The 2
are a prepared execute, which is still pipelined behind the failed
request. After a failed simple query a prepared execute is written after
the `ReadyForQuery` (was before).
- The script of the report, release build: 0 of 100 rounds wrong in 3
runs (base 35, 37, 39. Official 1.4.2: 86, 84, 62). With the proxy: 0 of
50 (base 50, 1.4.2 50).
- `close()` between the `ErrorResponse` and the held `ReadyForQuery`, 5
kinds of failing query and 6 ways to close: all 30 settle the same on
1.4.2 and here. A FATAL `ErrorResponse`, which the server follows with a
close and no `ReadyForQuery`, also settles the same.

**Existing suites**, debug build, local PostgreSQL: every
`test/js/sql/postgres-*.test.ts`, `sql-prepare-false`,
`sql-pool-transaction-isolation`, `sql-reserve-abort`,
`sql-onconnect-onclose-throw`, `sql-close-pending-connection`,
`wire-frames`. `postgres-string-leak` exceeds the 5 s timeout under the
debug build, on base too. In the last run the container was under load,
and subprocess tests in 4 other files passed 5 s. Each of those that I
ran again alone passed. `sql.test.ts` against the local server: 18 to 20
of 854 fail, base 23. Each case that fails on the branch fails on base
too. They need roles and an encoding that the local server does not
have, or they depend on time under the debug build. The case for oven-sh#22395
passes. No run stopped at the new assertion.

**A request that fails in its Parse** stays `Pending` with its place in
`pending_requests`. The `discard_response` branch of the `ReadyForQuery`
arm calls `finish_request`, whose `Pending` arm releases that place.
`finish_request` has no edit.

**Covered by reading only.** The failure path of `on_result` needs a
command tag that fails to convert. It now keeps the request in flight
when the `ReadyForQuery` is still to come. Before, the counter of that
request was never released.

**Not changed, named for later.**
- A peer that sends a `ReadyForQuery` that answers no Sync releases a
held request one message early. The rows stay in order (the stop).
- The setter of `IS_READY_FOR_QUERY` and the early clear of
`WAITING_TO_PREPARE` at `ParseComplete` and `ParameterDescription`. The
held counters are the guard. A count of Syncs is the follow-up if the
flag must be exact. It needs a rule for a `ReadyForQuery` that answers
no Sync first: pgdog sends one, and 6 cases of
`postgres-infinity-date.test.ts` answer one Sync with two.
- Parse+Describe+Sync of a named statement and the first execution
without parameters hold no counter. The flag pair guards them.
- The arm of `advance()` for a request with no statement. It cannot be
reached: `do_run` sets the statement before each push.
- The MySQL adapter releases a request at a row it cannot decode, in the
middle of its response (`JSMySQLConnection.rs`). oven-sh#43323 is open for
that.

**Open pull requests on the same code.**
- oven-sh#35120 (re-prepare of invalidated statements) clears
`IS_READY_FOR_QUERY` in the `ErrorResponse` arm for every error. That
line alone closes the gap for a server that sends one `ReadyForQuery`
for each Sync. It leaves the early release and the step-over. By
reading, the pgdog case stays with that line alone. The two changes
merge with no conflict. The retry branch of oven-sh#35120 releases and queues
the request again at the `ErrorResponse`, so it needs the same hold
after a rebase.
- oven-sh#33370 (`Query.cancel()`) releases a `Pending` request whose Parse is
on the wire and marks it `Fail`. That is the same early release from a
second place.
- oven-sh#43918 rewrites `encode_request`. This change does not touch it.

**Self-review.** The concerns taken up:
1. The first text said that no open pull request covers this gap. oven-sh#35120
covers a part of it. The text above now says so.
2. oven-sh#33370 was not named. It is named above.
3. The sites of the same class that stay were not listed. They are
listed above.
4. Each half was pinned only by a mock of a peer that does not exist.
Addressed in part: the stop test now replays the frames of pgdog, and
the run against the real pgdog is above. The hold test is still a mock.
5. `src/` had comments of more than one line. Each is one line now.
6. The reserved case ran on a pool of 1. It now reserves from a pool of
10: 10 sockets open and one carries the queries.
7. Nothing stated the write order in a debug build. `advance()` now
asserts that it passes no request that is not written yet.

**Credit.** The hold is the mechanism of oven-sh#43187. The stop is the arm of
oven-sh#20986.

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 4 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 19 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/sql/postgres-prepared-pipeline-reorder.test.ts
bun test v1.4.3 (367d939)

test/js/sql/postgres-prepared-pipeline-reorder.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > a prepared-statement execute does not jump a queued unwritten prepare [950.35ms]
(pass) postgres > prepared executes do not jump multiple queued unwritten prepares [371.61ms]
(pass) postgres > mixed burst of prepared and new statements returns every row in order [369.38ms]
(pass) postgres > a prepared-statement execute does not jump a queued unwritten simple query [128.05ms]
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
204 |   return `{${values.map(arrayValueSerializer.bind(this, type, isPostgresNumericType(type), isPostgresJsonType(type))).join(delimiter)}}`;
205 | }
206 | function wrapPostgresError(error) {
207 |   if (Error.isError(error)) {
208 |     return error;
209 |   return new PostgresError(error.message, error);
               ^
PostgresError: Failed to read data
 hint: "The quer
... (truncated)

release without fix: all passed
bun test v1.4.3-canary.1 (21c547a)

test/js/sql/postgres-prepared-pipeline-reorder.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > a prepared-statement execute does not jump a queued unwritten prepare [54.98ms]
(pass) postgres > prepared executes do not jump multiple queued unwritten prepares [48.48ms]
(pass) postgres > mixed burst of prepared and new statements returns every row in order [24.94ms]
(pass) postgres > a prepared-statement execute does not jump a queued unwritten simple query [12.04ms]
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > before the ReadyForQuery of a query that failed behind another query > a new statement with other column types, a prepared one resolve with their own rows [131.62ms]
(pass) postgres > before the ReadyForQuery of a query that failed behind another query > a new statement, a prepared one, through execute() resolve with their own rows [141.14ms]
(pass) postgres > before the ReadyForQuery of a query that failed behind another query > a new statement, a simple query resolve with their own rows [142.52ms]
(pass) postgres > before the ReadyFor
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/sql/postgres-prepared-pipeline-reorder.test.ts
bun test v1.4.3 (367d939)

test/js/sql/postgres-prepared-pipeline-reorder.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > a prepared-statement execute does not jump a queued unwritten prepare [609.58ms]
(pass) postgres > prepared executes do not jump multiple queued unwritten prepares [492.29ms]
(pass) postgres > mixed burst of prepared and new statements returns every row in order [253.06ms]
(pass) postgres > a prepared-statement execute does not jump a queued unwritten simple query [157.56ms]
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > before the ReadyForQuery of a query that failed behind another query > a new statement, a prepared one resolve with their own rows [1856.81ms]
(pass) postgres > before the ReadyForQuery of a query that failed behind another query > a new statement with other column types, a prepared one resolve with their own rows [1664.26ms]
(pass) postgres > before the Rea
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 7581ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/186] gen cpp.rs (cppbind)
[2/186] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 248 extern-C blocks audited
[3/186] gen JS modules (bundle-modules)
Preprocess modules (11769ms)
Bundle modules (1818ms)
Postprocesss modules (245ms)
Bundle Functions (1012ms)
Generate Code (63ms)

[14.93s] Bundled "src/js" for production
  2613 kb
  198 internal modules
  13 native modules
  50 internal functions across 16 files
[4/74] cxx obj/unified/UnifiedSource-src_jsc_bindings-1.cpp.o
[5/74] cxx obj/unified/UnifiedSource-src_jsc_bindings-3.cpp.o
[6/74] cxx obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o
[7/74] cxx obj/unified/UnifiedSource-src_jsc_bindings-5.cpp.o
[8/74] cxx obj/unified/UnifiedSource-src_jsc_bindings-4.cpp.o
[9/74] cxx obj/src/jsc/bindings/bindings.cpp.o
[10/74] cc obj/packages/bun-usockets/src/loop.c.o
[11/74] cxx obj/unified/UnifiedSource-src_jsc_bindings_node-0.cpp.o
[12/74] cc obj/packages/bun-usockets/src/bsd.c.o
[13/74] cxx obj/un
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/sql_jsc/postgres/PostgresSQLConnection.rs      |  17 +-
 src/sql_jsc/postgres/PostgresSQLQuery.rs           |  16 +-
 .../sql/postgres-prepared-pipeline-reorder.test.ts | 278 ++++++++++++++++++++-
 test/js/sql/wire-frames.ts                         |  56 +++++
 4 files changed, 355 insertions(+), 12 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                    reads  edits  tests
src/sql_jsc/postgres/PostgresSQLConnection.rs               5      0     39
src/sql_jsc/postgres/PostgresSQLQuery.rs                    3      0     39
test/js/sql/postgres-prepared-pipeline-reorder.test.ts      2      0     38
test/js/sql/wire-frames.ts                                  2      2     40
```

</details>

<!-- robobun:evidence:end -->
…ven-sh#43323)

### Problem

- `Bun.SQL` (MySQL): when the client cannot decode a row, its query
rejects and the rest of the response goes to the next query. After a
`CALL` with two result sets, the next query resolves with
`[{"leftover":777}]`, or the connection fails with
`ERR_MYSQL_UNEXPECTED_PACKET`.
- `JSMySQLConnection::on_result_row`
(`src/sql_jsc/mysql/JSMySQLConnection.rs`) marked the query `Fail` at
the bad row. `advance()` pops a `Fail` head at the next terminator,
while the server still answers it.
- A JSON column that `JSON.parse` refuses fails in `to_js`, which
returned `JSError`. That closes the connection: every queued query
rejects with that `SyntaxError`. A default MariaDB stores such text
(`{"name":"O\'Brien"}` passes `JSON_VALID`).

### Fix

- The query rejects at once but stays at the queue head, because the
server still sends the rest of its response. The new `DISCARD_RESPONSE`
flag skips those rows. The last terminator, a server error, or a close
marks it `Fail`, with no second rejection.
- A `to_js` error takes the same path. A pending termination exception
still fails the connection.
- Verified: `test/js/sql/sql-mysql-row-decode-error.test.ts` (9 mock
tests, 1 MariaDB test, all fail without the fix). Also
`test/js/sql/sql-mysql*.test.ts`.
- Self-reviewed: 9 concerns raised, 7 addressed. Not taken: a merge into
oven-sh#43187, the same fix for Postgres (linked instead), and issues for the
"Not covered" sites.

### Background

- `Bun.SQL` pipelines: it writes several queries before the first reply.
Each reply packet goes to the head of a FIFO queue.
- A response is one or more result sets. Each ends with a terminator.
Its `SERVER_MORE_RESULTS_EXISTS` flag says that another follows.
- `advance()` pops finished requests from the queue head and writes
queued ones.

<details><summary>Notes</summary>

**Reproduction.** Mock server, 1.4.3 (`b52d51348`), `max: 1`, before the
fix:

```
A (decode error, two result sets)
  bad query : REJECTED ERR_MYSQL_INVALID_BINARY_VALUE: Failed to bind query
  neighbour : [{"leftover":777}]
B (to_js error)
  bad query : REJECTED SyntaxError: JSON Parse error: Expected '}'
  neighbour : REJECTED SyntaxError: JSON Parse error: Expected '}'
```

**MariaDB.** A JSON column is `LONGTEXT` with a `JSON_VALID` check
constraint. On MariaDB 11.8.6 with default settings, `JSON_VALID`
accepts `\'`, `\x41`, `\q` and a Windows path with single backslashes
(`{"path":"C:\Users\qux"}`). `JSON.parse` refuses all of them. Before
the fix, one such row closed the connection: a pipelined query that the
server had answered rejected with `SyntaxError: JSON Parse error:
Invalid escape character '`, and a `TEMPORARY` table was gone for the
next query. Before v1.4.0 such a column came back as a string (oven-sh#37130
began to parse it). The MariaDB test in the new file covers this. It
runs where `mariadb_plain` is provided (the same gate as
`sql-mariadb-json.test.ts`). MySQL has a native JSON type and sends
normalized text, so only a mock reaches this path there.

The decode error (`row.decode`) needs a mock: a healthy server does not
send a binary `DATE` of 5 bytes. MySQL frames each row in a packet, and
`process_packets` skips the packet whole, so the stream stays in step
after any row decode error.

**Not covered, on purpose.**

- A column definition that cannot be decoded, at prepare time
(`handle_prepared_statement`) or at execute time (`handle_result_set`),
still fails the connection. A column type that `FieldType` does not know
reaches these sites with `ERR_MYSQL_UNSUPPORTED_COLUMN_TYPE`, for
example `MYSQL_TYPE_VECTOR` (0xf2) from MySQL 9. The fix there is to
decode an unknown type as bytes, not to reject the query, so it is a
separate change.
- A row whose lengths run past its packet still reads as `ShortRead`,
which means "wait for more data". oven-sh#35118 bounds reads to the packet.
After it, such a row takes the path of this PR.
- A result set header, `OK`, `EOF` or `ERR` packet that cannot be
decoded still fails the connection. The stream is out of step then.

**Overlap with open PRs.** oven-sh#33190 adds `mysqlBinaryResultSetRow` and
`mysqlBinaryResultSet` to `test/js/sql/wire-frames.ts` under the same
names. The versions here take one more optional argument
(`statusFlags`). The PR that lands second drops its copy. `settle()` in
the new test file is the helper from the test file of oven-sh#43187. The PR
that lands second can share it.

**Accounting.** `mark_current_request_as_finished` is no longer called
at the bad row. The last terminator, or the `ERR` packet, calls it while
the request still counts as running, so `pipelined_requests` or
`nonpipelinable_requests` drops once. A build that skips this release
for a discarding request makes 8 of the 9 mock tests time out: a
statement that is not prepared yet, and a `.simple()` query, can only
start after the counter drops.

**Other shapes I ran by hand on the ASAN debug build.**

- Real MariaDB: a 200000 row result whose first row is not JSON, with
`Bun.gc(true)` in the rejection handler while the rest streams. The same
with the bad row second to last.
- Real MariaDB: `sql.begin()`. The callback throws on the rejection and
`ROLLBACK` runs on the same connection. A callback that catches the
rejection continues in the same transaction.
- Real MariaDB: `.values()` rejects the same way. `.raw()` does not
decode and resolves.
- Mock: the legacy framing without `CLIENT_DEPRECATE_EOF`.
- Mock: the server drops the connection, or `sql.close({ timeout })`
fires, while the response is discarded. The query rejects once and the
next one rejects with `ERR_MYSQL_CONNECTION_CLOSED`.

**Test runs** on the debug build against a local MariaDB:
`sql-mysql.test.ts` (plain image only) gives 97 pass and 7 fail,
`sql-mysql.transactions.test.ts` gives 12 pass and 2 fail. The same
tests fail on the build without the fix. They compare MySQL error texts
that MariaDB words differently, or they exceed their timeout under ASAN.
`BUN_JSC_validateExceptionChecks=1` passes on the new file.

</details>
Wait for the Node HTTP listener and all 32 body-validated fetches before disposing the server. Replace the unjoined timer and vacuous Infinity comparison with isolated Response and AbortSignal reclamation checks. The original fixture fails with ERR_SERVER_NOT_RUNNING; a retained-response control fails the finite object bound.
Start renegotiation only after the first client handshake callback sends a framed ready message. Keep the framed payload acknowledgement, exact two-handshake assertion and pauseOnConnect regression check. This removes a setup race without changing TLS runtime code.
Import the PostgreSQL and MySQL query-isolation fixes as an additional merge. Preserve the published OpenClaw WebKit pin and all previously qualified fork changes; append the new upstream target to the changelog.
@steipete steipete changed the title chore: sync upstream Bun with the rebased OpenClaw WebKit chore: sync upstream Bun with paired OpenClaw WebKit Oct 5, 2026
@steipete
steipete marked this pull request as ready for review October 5, 2026 08:35
@steipete
steipete merged commit 9ecd8d0 into main Oct 5, 2026
13 of 14 checks passed
@steipete
steipete deleted the claude/w150-upstream-webkit-sync branch October 5, 2026 08:37
steipete added a commit that referenced this pull request Oct 5, 2026
OpenClaw's catalog performance consumers reach `HeapProfiler.startSampling`, which the in-process inspector did not implement. Add `startSampling`, `getSamplingProfile`, and `stopSampling` for callback and promise Sessions using the real per-VM JSC allocation sampler from the immutable [OpenClaw WebKit release](https://github.com/openclaw/WebKit/releases/tag/autobuild-42ab38d705d4838748ccee77e7deb0e4e35515ee) integrated by #124.

Preserve Node defaults, synchronous callbacks, repeated-start behavior, major/minor collection flags, worker isolation, and session cleanup. Profile retrieval forces full collection. Numeric validation uses an immutable intrinsic and truncation happens in the native binding, so replacing public numeric helpers cannot violate the engine's interval precondition. Sampling through a worker's `connectToMainThread()` remains unsupported. Upstream's CLI heap-profile work in oven-sh#34842 is separate from these Session commands.

Validation on Linux x64, with unmodified OpenClaw `b02eab15853b6471d1869f2a2cb0f135b7aefe4d` and Node 24.21.0 controls:

- Inspector suite: 78 passed, zero failures.
- Plain fork selection: 25 files, 27/27 result rows passed.
- Gateway and Codex native catalog performance consumers: each passes on the candidate and Node; the earlier baseline fails both at missing `startSampling`.
- The original sampler patch fails both public numeric-helper override cases; the corrected candidate and Node pass.
- Pinned formatting checks and independent scoped P2 review pass.

The committed production manifest is unchanged by this PR and selects the published engine. No runtime publication is included.

Final head `c8de1bfd252a50c03286cb83d7e3a73cca6b5395` passes [both native CI lanes](https://github.com/openclaw/bun/actions/runs/37292758243), formatting, source lints, and built-in module typechecking. The complete standalone Linux proof used `46754a481e350a63b64ac3edd5449ef1056604ba`; the final follow-up adds only the missing TypeScript declaration for the intrinsic, with unchanged runtime code. The final head also builds successfully on macOS arm64.
steipete added a commit that referenced this pull request Oct 5, 2026
Connect Bun's stack reporting and transpiler to the syntax positions in the published OpenClaw WebKit engine integrated by #124. Calls, property reads, constructors, and async continuations use the engine's selected locations; returning built-in error constructors retain their frames and `new` syntax. Runtime transpilation preserves callee parentheses and computed access and maps call, bracket, and template delimiters back to the original source.

Bump the runtime transpiler cache format to 38 so cached output from earlier versions cannot retain the old positions. Keep existing file-name and eval-origin formatting. Update the ErrorEvent inspection caret to the `new` token confirmed by Node. This continues the source-position work in oven-sh#35179, oven-sh#37396, and oven-sh#41580; thanks @robobun.

Native macOS arm64 validation on candidate `8b774531c290dac5eeb35cd62b7bce517676a2e5`:

- Stack, util, and source-map suites: 343 passed.
- Minifier suite: 68 passed. Inspection suite: 95 passed, one existing skip.
- Runtime cache replacement and marked cached-output replay pass at version 38, preserving all 56 coordinate assertions.
- Rust checks: all 12 targets pass, with zero skips. Independent scoped P2 review and source lint pass.

Unmodified OpenClaw `b02eab15853b6471d1869f2a2cb0f135b7aefe4d` consumers pass on the candidate and Node 24.21.0: SDK alias 67 passed with one existing skip; ordered loader pair 8 passed, with execution order, shared worker and canonical test policy verified.

The plain Darwin selection has zero new regressions on the local macOS 27 host: candidate 20/21 rows versus baseline 19/21. The candidate's only failed file contains two socket-cancellation assertions reproduced unchanged with the pre-stack binary in isolation and the full selection. A baseline-only HTTP failure did not recur on the candidate. The fork-pinned Node 26.3.0 harness is used; the OpenClaw consumer controls use Node 24.21.0. No assertions or selections were weakened. Both exact-head native CI lanes pass in [run 37298205240](https://github.com/openclaw/bun/actions/runs/37298205240).

The cache oracle improves from 2/56 matching positions on the baseline to 56/56 on both candidate runs. The committed production WebKit manifest remains unchanged.

Formatting, source lints, JavaScript typechecking, Clippy, Miri and cargo tests pass. The Rust workflow succeeds; its explicitly advisory mordant job reports the unchanged preexisting bare-bool-argument style finding in `src/resolver/package_json.rs`, with no suppression or baseline change in this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants