Skip to content

chore(ci): build with Bun 1.4.4 (drops the compromised eMudhra root) - #2215

Merged
backnotprop merged 22 commits into
mainfrom
chore/bun-1.4.4
Oct 27, 2026
Merged

backnotprop merged 22 commits into
mainfrom
chore/bun-1.4.4

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Bun 1.4.4 was released Oct 22. The owner made a one-time exception to the 7-day release-age rule for this security fix.

Why

Eight eMudhra intermediate CA keys were compromised in Oct 2026. Bun 1.3.14 embeds "emSign ECC Root CA - G3", and bun build --compile copies Bun's root list into every release binary. Bun 1.4.4 moves to NSS 3.131.1, which drops that root. It also adds Node's blocked public keys check for the compromised keys (oven-sh/bun#46300).

Credit

Changes

  1. Bun 1.3.14 → 1.4.4 in all 11 workflows: 20 setup-bun pins, plus the DAST BUN_VERSION and target image. Also the Renovate pin note and the build doc. I re-read the DAST digest from the registry: oven/bun:1.4.4-slim index sha256:1846adf22496bd711036aae8be147e57bdebbaea2e5264e3e0b5af1db187d9a4. docker buildx imagetools inspect and the Hub API agree, and docker run on that digest prints 1.4.4. There is no packageManager field, no .bun-version, and no Bun pin in scripts/install.*.

  2. Tests (from chore: upgrade bun runtime to 1.4.2 #1489). In Bun 1.4, as in Node, every node:http Server gets its own "listening" listener when it is created. The Pi port tests now count listeners from that baseline instead of from 0. waitForFile now waits until the file has content, not only until it exists.

  3. Pi: end open event streams before stop() destroys their sockets. Pi calls closeKeepingDraft() and then stop() right away.

    • closeAllConnections() dropped the session-closed write, so the open tab never learned that the review closed. This also happens on Node 24, Pi's real runtime.
    • Bun 1.3's closeAllConnections() left the stream open, so decision-listener-throw.test.ts passed only on Bun 1.3.
    • stop() now ends the streams first, in the plan, annotate and review servers. I checked this on Node 24, Bun 1.3.14 and Bun 1.4.4.
  4. patches/@opencode-ai%2Fsdk@1.19.3.patch. The SDK's SSE abort handler calls void reader.cancel().

    • On Bun 1.4 that promise rejects with AbortError, nothing handles the rejection, and the process ends. In the compiled server that would happen on every OpenCode Ask AI answer or Stop.
    • Node 24 and Bun 1.3.14 resolve the promise. SDK 1.19.4 has the same code.
    • The patch catches the rejection in the v1 and v2 clients. It is applied in the compiled binary and in the OpenCode plugin's embedded.js.
    • bun.lock changes only by its patchedDependencies entry.
  5. Two DOM checks that Bun 1.3 passed while they failed. On Bun 1.3.14, expect(el).toBeNull() passed for a React-attached element when the failure message was too long to format. Bun 1.4.4 reports "The error message exceeds the maximum string length".

    • DiagramViewer left the composer open when the host turned commenting off. It now closes it when canCreate becomes false.
    • compose.test.tsx expected the Drafts count to be 0, but the seeded world has an unsent reply. It now asserts the server's count.
    • The failing first test also made the 1.4 runner kill the beforeAll server ("killed 1 dangling process"). That cascaded into 13 ECONNREFUSED failures.
    • The guides.show viewer bundles DiagramViewer, so packages/core/guide-viewer-manifest.ts is re-pinned. Without this change, Bun 1.4.4 reproduces main's manifest byte for byte, so the bump itself does not move it.
  6. Inbox: the stream watchdog is 5 minutes, not 30 s. Bun 1.4 reads a streamed body only as fast as the client takes it. Bun 1.3.14 read the whole range into memory at once.

    • So a player paused with a full buffer leaves a live answer unread, and the 30 s watchdog cut it short. Measured with a 2 s watchdog: a 200 KB/s client lost its 8 MiB answer at 14 s on 1.4.4, never on 1.3.14.
    • Bun 1.4.4 also fixed what the watchdog was for: it cancels clients that left before the answer started, and FileHandle.close() during a read closes the descriptor. The watchdog is now a backstop.
    • The 8 MiB cap per answer stays.
  7. Release gate: scripts/check-bun-runtime-trust.ts. In release.yml, before upload, every compiled binary must meet three conditions:

    • it embeds Bun >= 1.4.4;
    • it does not hold emSign ECC Root CA - G3 as PEM, DER or public key;
    • it has Bun's blocked-key check (marker addBlockedSPKIForTesting; the eight key hashes are compiled into code and cannot be searched).

    The gate reads bytes only, so the Linux builder also checks the darwin and Windows binaries. It requires at least 12 binaries. Locally, all 12 1.4.4 binaries (6 CLI, 6 paste) pass, and the 1.3.14 builds fail all three checks. Synthetic-buffer unit tests are included.

  8. Older-CPU smoke control. In Bun 1.4.4, bun-linux-x64 and bun-linux-x64-baseline are byte-identical (sha256 10702245766b…ac235 in both signed release zips). So the default-target control binary no longer gets SIGILL under qemu -cpu Nehalem, and the "must SIGILL" control failed.

    • The control is now one AVX2 instruction built with gcc on the runner. It runs natively and must exit 132 under the same qemu command.
    • Checked in an amd64 container: it exits 132, the same as a Bun 1.3.14 default-target build. 1.3.14 and 1.4.4 baseline builds run there.
    • The control binary's build, upload and download steps are removed.
  9. Host runtime floors. These only produce warnings; they never block an install.

    • OpenCode plugin: engines.bun >=1.4.4.
    • Pi extension: engines.node >=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1. These are the Oct 20–21 Node security releases with NSS 3.131.1 and tls: reject chains containing blocked public keys nodejs/node#66871.
    • @plannotator/server (private) drops its bun >=1.0.0 peer, so the 13 bun@1.4.0 / @oven/bun-* entries leave bun.lock.
    • Both READMEs state the floor.
  10. No compile path builds on an older Bun. scripts/require-bun.ts fails on Bun < 1.4.4. The t3-code build and tests/manual/local/sandbox-codex.sh run it first. tests/manual/ssh/Dockerfile installs bun-v1.4.4. The build guide now says ">= 1.4.4" (it said ">= 1.3.14") and documents the gate.

  11. Renovate. Minor and major bun-version bumps stay off. A Bun patch release now opens its own PR, which is never automerged and waits out the 7-day release age. The PR body lists the checks to run and the dast.yml bump.

  12. Comments re-measured on 1.4.4. Each was probed on both versions:

    • bunfig [loader]: the Decision mode: the agent contract lines on the Queue card, and #2141's review follow-ups #2154 cache bug does not happen on 1.4.4. The loader stays.
    • gh-tripwire: Bun.spawn, Bun.spawnSync and Bun.which still use the environment the process started with, so the wrappers stay. Node's sync spawns and Bun Shell lookups now use the current environment.
    • diagram-engine: a type: "text" import now costs about 2 ms (2.6 s on 1.3.14).
    • IPv6 hostname brackets: unchanged.
    • Inbox byte-range facts: rewritten to say what each version does.
  13. Two Inbox tests that a GC could break on 1.4.4. Bun 1.4.4 closes the connection of a streamed Response that is garbage-collected while nothing reads it; 1.3.14 keeps it open. inbox-notify-origin and inbox-shared-windows dropped their event-stream responses after one read, so a GC mid-test closed a tab. CI saw this once: windows: 1 where the test asserted 2. Both tests now hold their responses until they abort them. A reader that keeps a read pending (every production client loops or uses for await) survives continuous GC on both versions.

  14. Main's new Inbox tests under Bun 1.4.4 (merged origin/main, no rebase):

    • boardPage.test.tsx, phone: toBeNull() on the folded Shipped card passed on 1.3.14 only because the failure was too long to format. On 1.4.4 the failure printed 2.77 GB of output, and CI's test job hit its 45-minute limit. A folded group keeps its body in the DOM, hidden, because aria-controls points at it. So the component is right and the test now asserts hidden, then shown, as plain words.
    • boardSend.test.tsx pressed Send before the window's live-sessions read was answered. On 1.4.4 the answer lands just after the click, so the picker never opened, the test timed out, and the runner killed the shared server (ECONNREFUSED for the rest). The test now waits for that answer.
    • tour.test.tsx (also notifyHook and firstRunNotifications) restored window.Notification = undefined, which 'Notification' in window reads as support. A read still in flight after the unmount then hit Notification.permission: an unhandled error between tests, 4 of 6 runs on 1.4.4. The tests now delete the property, and notify.ts treats only a Notification constructor as support.
  15. @opencode-ai/sdk pinned to exactly 1.19.3, the version its patch is for. A ^ range would let a bump drop the patch silently.

Binary size

On Bun 1.4.4 the CLI grew 53% (darwin-arm64 158 → 243 MB). The three app pages (index.html, review.html, inbox.html, about 64 MB together) were imported with { type: "text" }. Bun 1.4 stores such an import, if it is not pure ASCII, as its own embedded file in UTF-16, which doubles it. Our pages have curly quotes and other non-ASCII text.

  • Ruled out: sourcemaps, bytecode and duplicated assets. --minify saves 3 MB, --sourcemap=none saves nothing, and Bun has no flag for this.
  • The rest of the growth is Bun's runtime itself (+26 MB on macOS).

Fix (chosen): import the pages with { type: "file" } (stored as built), and read each into the same string when a server first needs it (embeddedPage). Servers get exactly what they got before.

Alternative measured and rejected: escape the pages to pure ASCII and keep type: "text". It came to 180.1 MB with slightly faster first fetches, but:

  • Latin-1 is not enough (it stays 243 MB);
  • it needs about 110k escapes per page, correct in JS, CSS, regex and HTML contexts;
  • the served bytes change.

Inventory of embedded assets:

  • The 3 pages (changed).
  • Diagram engine ×3 and Inbox tour ×15: already type: "file".
  • The agent-terminal sidecar: a 5 KB ASCII type: "text", unchanged.
  • 20 call-flow lockfiles: type: "json", unchanged.
  • The paste service embeds nothing.

Sizes (MB)

0.28.31 (Bun 1.3.14) 1.4.4 unfixed 1.4.4 fixed
CLI darwin-arm64 157.7 242.9 179.2
CLI darwin-x64 162.7 250.7 187.5
CLI linux-x64 187.1 261.6 198.4
CLI linux-arm64 187.1 261.7 198.5
CLI win32-x64 191.3 267.8 204.6
CLI win32-arm64 188.1 252.7 189.5
paste darwin-arm64 63.4 89.2 89.2
paste darwin-x64 69.2 98.2 98.2
paste linux-x64 93.6 109.1 109.1
paste linux-arm64 93.7 109.2 109.2
paste win32-x64 97.8 115.3 115.3
paste win32-arm64 94.6 100.2 100.2

What remains over 0.28.31 is Bun 1.4.4's own runtime; the paste rows grow by exactly that.

Performance (darwin-arm64, medians of 12 runs, round-robin, sandboxed; 0.28.31 is the verified release binary)

0.28.31 1.4.4 unfixed 1.4.4 fixed
--version 427 ms 181 ms 174 ms
--version max RSS 563 MB 240 MB 112 MB
review: server ready 985 ms 727 ms 673 ms
annotate: server ready 536 ms 271 ms 275 ms
review GET /: first, TTFB/total 4.9/7.6 ms 4.8/8.3 ms 5.3/7.6 ms
review GET /: repeat 2.7/5.0 ms 2.3/4.8 ms 2.3/4.8 ms
annotate GET /: first 5.6/9.2 ms 5.6/8.8 ms 7.1/10.4 ms
annotate GET /: repeat 3.5/6.8 ms 3.1/6.3 ms 3.3/6.5 ms
review server RSS, idle → after serving 626 → 642 MB 267 → 277 MB 202 → 210 MB
annotate server RSS, idle → after serving 572 → 595 MB 245 → 256 MB 214 → 236 MB

The fixed build's first fetch of the 24 MB annotate page is about 1.8 ms slower than unfixed. That is statistically real over 20 extra rounds, and repeats are equal. In remote mode (first gzip request), fixed and unfixed are within noise.

Same bytes and headers. The fixed and unfixed builds send byte-identical status lines, headers and bodies in every mode:

  • plan review (hook stdin);
  • annotate: file, folder, URL, --static, last message;
  • archive and setup-goal;
  • code review, plus its diagram engine files;
  • Inbox;
  • guide export;
  • the paste service.

Each mode was checked local and remote, with no Accept-Encoding, gzip, br, and gzip, deflate, br. Every decoded body equals the built file. Compared with 0.28.31, the header names and values are the same, except x-plannotator-server: 1, which main added after 0.28.31. Compression and caching are unchanged: the gzip prewarm, br on demand, local sessions never compressing, the diagram engine's immutable Cache-Control, and the Inbox's no-store. Pi and OpenCode serve their own copies of the built HTML and do not go through this code.

Self-update safety. Bun reads embedded files from what it loaded at startup, never from the binary's path. A first read after the binary was replaced returns the running binary's own bytes:

  • macOS: when it was renamed over, deleted, or even overwritten in place;
  • Linux: when renamed over or deleted (the kernel refuses an in-place write).

CI's smoke-binaries now checks this on macOS, Linux and Windows with scripts/smoke-pages.mjs. It runs annotate from an installed copy, replaces that copy the way the OS's installer does (install.sh: rename over; install.ps1/install.cmd: move the running exe to .old), and then fetches the page for the first time. A probe compiled on the runner shows the same for a first read of an embedded file. The script also compares every served page with the build job's digests, so a page that changes in the binary fails the release.

Verification (local, macOS arm64)

I downloaded Bun 1.4.4 into a temp dir from the GitHub release (bun-darwin-aarch64.zip). The system bun was not changed. SHASUMS256.txt.asc has a good signature from Robobun F3DCC08A8572C0749B3E18888EAB4D40A7B22B59, and the zip's SHA-256 (c240d7f0…bc510) matches the signed list. All tests ran with a temp HOME and PLANNOTATOR_DATA_DIR, NO_BROWSER, PLANNOTATOR_UPDATE_CHECK=0 and PLANNOTATOR_INBOX_CONNECT=0.

  • bun install --frozen-lockfile, check:release-version, typecheck and VS Code lint pass.
  • bun test at 054c826: 10518 pass, 2808 skip, 0 fail.
  • Earlier bun test runs:
    • Before the fixes: 6 failures, all of which pass on Bun 1.3.14 (the 3 Pi listener counts, the Pi session-closed test, the 2 OpenCode real-client tests).
    • At the head: 10433 pass, 2768 skip, 1 fail. The failure is pi WSL configured browser launch timing out (2 s waitForFile) while other test runs were loading the machine. It passes 15/15 in isolation on both 1.3.14 and 1.4.4.
    • Under a preload that runs Bun.gc(true) every 25 ms, all 246 packages/server and Pi test files pass on 1.4.4 (see change 13).
  • CI's DOM test steps, the whole test.yml list after merging main, on 1.4.4: file-browser 6 pass; diff-renderer 40 pass; --isolate set 2903 pass, 0 fail, 0 errors; allowlist guard passes. Before the fixes, the --isolate set had 14 failures from this branch, plus main's Board and tour tests.
  • Build in order (review, hook, opencode, pi) passes. Playwright e2e on a dev binary: inbox, review-diagrams, annotate-html-links, annotate-bundle-back-scroll, image-region-comments and toolstrip-stacking, 30 passed (CI's Inbox window job runs the whole list). guides-show build:viewer gives the same manifest locally and in CI.
  • Signed macOS builds (QA item 9).
    • I built plannotator-darwin-arm64 and plannotator-paste-darwin-arm64 two ways: cross-compiled from Linux in the pinned oven/bun:1.4.4-slim image, as release.yml does, and natively.
    • codesign --verify --strict --verbose reports "valid on disk" and "satisfies its Designated Requirement", with an ad-hoc, linker-signed signature (flags 0x20002).
    • Each binary ran from a cwd with a hostile bunfig.toml:
      • --version prints plannotator 0.0.0-dev, and --help works.
      • Headless plannotator review on a scratch git repo serves /api/diff with the change.
      • annotate serves /api/plan, and the paste service starts.
    • release.yml has no codesign or notarization step for these binaries. They ship with Bun's ad-hoc linker signature, and its smoke-binaries macOS leg runs them.
  • The root is gone. tls.rootCertificates in a compiled probe:
    • Bun 1.3.14: 145 roots, including emSign ECC Root CA - G3 (fingerprint 86:A1:EC:BA…:6B:BE:6B), emSign Root CA - C1 and emSign ECC Root CA - C3.
    • Bun 1.4.4: 118 roots and none of those three. ISRG Root X1 is present in both.
    • A byte scan of the compiled CLI and paste binaries confirms this. On 1.3.14 the cert is 1 of the 146 embedded PEM roots (an exact DER match after decoding). On 1.4.4 it does not match as DER, SPKI, raw EC key or PEM. The control, ISRG Root X1, is found as DER in the same 1.4.4 binaries, so the scan works. The release gate above now runs this check on every release binary.
    • Bun 1.4.4 still trusts emSign Root CA - G1 (RSA), which is still in NSS 3.131.1.

CI

At the head, every check has passed, including:

  • the 3 smoke-binaries jobs (macOS arm64, Linux x64 with the AVX2 control, Windows x64);
  • install-script-smoke;
  • build, with the trust gate: OK for all 12 binaries;
  • test and the DOM steps;
  • the guides.show manifest check.

Inbox window e2e also passed.

Binary size: fixed in this PR (see "Binary size" below).

backnotprop and others added 16 commits October 26, 2026 14:38
Bun 1.3.14 embeds "emSign ECC Root CA - G3", one of the roots the
compromised eMudhra intermediates chain to, and `bun build --compile`
copies that root list into every release binary. Bun 1.4.4 ships the
NSS 3.131.1 root list without it and adds Node's blocked public keys
check (oven-sh/bun#46300).

Moves every workflow pin from 1.3.14 to 1.4.4, including the DAST
target image (oven/bun:1.4.4-slim, index digest
sha256:1846adf2...87d9a4, re-read from the registry), and updates the
Renovate pin note and the build doc.

Taken in-house from #2214.

Co-authored-by: iMacTia <2527520+iMacTia@users.noreply.github.com>
…tten files

Bun 1.4, like Node, gives every node:http Server its own "listening"
listener at construction (connection tracking), so the Pi port tests
assert that listenOnPort leaves no listener of its own rather than
that there are none. Fixes three failures on 1.4.4.

waitForFile (agent-terminal and the Pi browser-launch tests) now waits
for content: a fake program's `> file` creates the file before printf
writes it, and a read in between saw it empty (seen once on 1.4.4).

From #1489.

Co-authored-by: FNDEVVE <141281206+FNDEVVE@users.noreply.github.com>
closeKeepingDraft() sends the open tab session-closed and Pi calls
stop() right after; stop()'s closeAllConnections() destroys the socket
and drops that write. On Node 24 (Pi's runtime) and Bun 1.4 the tab
never learns the review closed; Bun 1.3's closeAllConnections() left
the stream open, which is why decision-listener-throw.test.ts passed.
stop() now ends every open stream first, which sends it (checked on
Node 24, Bun 1.3.14 and 1.4.4). Plan, annotate and review servers.
…s on Bun 1.4

The SDK's SSE abort handler calls `void reader.cancel()`. On Bun 1.4 the
aborted fetch has already errored the body, so cancel() rejects with
AbortError, unhandled, and an unhandled rejection ends the process: the
compiled server on every OpenCode Ask AI answer or Stop. Node 24 and
Bun 1.3.14 resolve it. @opencode-ai/sdk 1.19.4 has the same code.

patches/@opencode-ai%2Fsdk@1.19.3.patch catches it (v1 and v2 clients).
The "through the real client" tests in opencode-sdk.test.ts fail on
1.4.4 without it. bun.lock only gains the patchedDependencies entry.
Bun 1.3.14's expect(x).toBeNull() passed for an element too big to
print (a React-attached node), so two DOM checks were green while
false. Bun 1.4.4 reports them.

- DiagramViewer: a host that takes commenting away (no handler, no
  reason) left the open composer up as a Comment form nothing could
  save. It now closes, as it does when the source pane closes.
- Inbox compose: the seeded world has an unsent reply (a picked
  question), so the Drafts row is right to show 1 after Discard. The
  check now asserts the row counts only those replies.
After compiling, before any upload: every release binary, paste binary
and the baseline control must embed Bun >= 1.4.4, carry the blocked-key
check (oven-sh/bun#46300; its test hook addBlockedSPKIForTesting is the
marker, the eight key hashes are folded into code), and hold no emSign
ECC Root CA - G3 as PEM, DER or public key.

scripts/check-bun-runtime-trust.ts reads bytes only, so it checks the
darwin and Windows binaries on the Linux builder too. Checked locally:
all six 1.4.4 targets pass; 1.3.14 CLI and paste builds fail on all
three counts.
Since Bun 1.4, bun-linux-x64 and bun-linux-x64-baseline are one
byte-identical runtime (1.4.4: sha256 10702245...ac235 in both release
zips), so the default-target control build ran on qemu -cpu Nehalem
and failed the "must SIGILL" control on this branch.

The control is now a one-instruction AVX2 probe built on the runner:
it runs natively (where the CPU has AVX2) and must die with SIGILL
under the same qemu invocation, which is what makes "the baseline
binary ran there" falsifiable. The control binary's build, upload and
download go. Checked in an amd64 container: the probe exits 132 under
-cpu Nehalem, as a Bun 1.3.14 default-target build does.
- OpenCode plugin engines.bun >=1.4.4 (first Bun that drops emSign ECC
  Root CA - G3 and blocks the compromised keys).
- Pi extension engines.node >=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1:
  the Oct 20-21 Node security releases (NSS 3.131.1 roots, tls: reject
  chains containing blocked public keys, nodejs/node#66871).
- @plannotator/server drops its `bun >=1.0.0` peer. It is private, and
  the peer only resolved bun@1.4.0 and its @oven/bun-* packages into
  bun.lock; those 13 entries go (hand-edited: a re-save also re-nests
  unrelated @effect entries). `bun install --frozen-lockfile` passes.

Bun and npm only warn on engines: a warning and documentation, never a
blocked install.
- scripts/require-bun.ts fails a compile on Bun < MIN_BUN_VERSION; the
  t3-code build and tests/manual/local/sandbox-codex.sh run it first.
- tests/manual/ssh/Dockerfile installs bun-v1.4.4, not whatever is
  latest.
- The build guide asks for bun >= 1.4.4 (was ">= 1.3.14") and says
  why; it documents the release trust gate.
- The Pi and OpenCode READMEs state the host runtime floors.
Minor and major bun-version bumps stay off (signed macOS builds need
checking first, v0.17.9). Patch releases, where Bun ships security
fixes, now raise their own "Bun runtime" PR after the 7-day release
age, never automerged, with a note: smoke-binaries (macOS) and the Bun
runtime trust gate must pass, and dast.yml's BUN_VERSION and image
digest are bumped by hand. 1.4.4's eMudhra fix would not have been
missed.
…r fix

The viewer bundles packages/ui's DiagramViewer, so the composer fix
changed viewer.js's hash. Regenerated with build:viewer + sync:manifest
on Bun 1.4.4; the CI build produced the identical manifest. Without
the DiagramViewer change, Bun 1.4.4 reproduces main's manifest byte
for byte: the bump itself does not move it.
Bun 1.4 pulls a streamed body only as fast as the client reads
(1.3.14 read the whole range ahead into memory). So a player paused
with a full buffer, or a slow download, now leaves a live answer
unpulled, and the 30 s idle watchdog cut it short; measured with a
2 s watchdog: a 200 KB/s client lost its 8 MiB answer at 14 s on
1.4.4, never on 1.3.14.

Bun 1.4.4 also fixed what the watchdog was for: it cancels a client
gone before the answer starts, and FileHandle.close() during a read
closes the descriptor. The watchdog stays as a backstop at 5 minutes.
The comments and the Inbox guide say what each Bun version does.
The 8 MiB cap stays: it bounds one answer on any runtime.
Each was probed on 1.3.14 and 1.4.4:
- bunfig [loader]: the cached-transpile bug that parsed tater-sidebar
  .webp as JS (#2154) reproduces on 1.3.14 for any extension with no
  loader, not on 1.4.4. The loader stays (intent, older Bun).
- gh-tripwire: Bun.spawn/spawnSync/which still use the startup env on
  1.4.4, so the wrappers stay; node's sync spawns and Bun Shell lookups
  now use the current env.
- diagram-engine: a `type: "text"` import of Mermaid costs ~2 ms on
  1.4.4 (2.6 s on 1.3.14); `type: "file"` still keeps the binary ~5 MB
  smaller.
- url-to-markdown: IPv6 hostnames stay bracketed on 1.4.4 and Node 24.
Bun 1.4.4 closes the connection of a streamed response that is
garbage-collected unread (1.3.14 keeps it open until the abort). The
notify-origin test dropped the second tab's response at once, and the
first one's reader after readStream, so a GC mid-test closed the tab:
CI saw `windows: 1` where it asserted 2. With a forced Bun.gc(true)
there it fails every time on 1.4.4 (`windows: 0`); holding both
responses until the abort passes on 1.3.14 and 1.4.4. The other
event-stream tests keep a reader with a read on the body, which holds.
Correction to the previous commit: one read on the body does not keep a
streamed response alive on Bun 1.4.4. openStream (and the door stream)
read once and drop the reader, so a GC closes the tab mid-test.

The responses are now held in a set until their controller aborts.
Checked with a preload that runs Bun.gc(true) every 25 ms over all 21
test files that read an event stream: on 1.4.4 only this file and
inbox-notify-origin failed, and both now pass on 1.3.14 and 1.4.4.
A reader that keeps a read pending (every production client loops or
uses `for await`) survives continuous GC on both versions.
- boardPage, phone: a folded group keeps its card in the DOM, hidden
  (its button's aria-controls names the body). `toBeNull()` on that card
  passed on Bun 1.3.14 only because the failure was too long to
  format; 1.4.4 prints the element, 2 GB of it, and CI's test job ran
  out its 45 minutes. The test now asserts the card is hidden, then
  shown after unfolding.
- boardSend: choosing Send opens the agent picker only if the live
  sessions are known. The test pressed Send before their read was
  answered; on 1.4.4 the answer lands just after, the picker stayed
  shut, the test timed out, and the runner killed the shared server
  (ECONNREFUSED for the rest). It now waits for that answer first.
Both pass on 1.3.14 and 1.4.4.
The patch (patches/@opencode-ai%2Fsdk@1.19.3.patch) applies to exactly
1.19.3; with ^1.19.3 a bump would drop it silently. bun.lock changes
only by that specifier; the patch still applies.

Also, boardPage's phone check asserts a word (absent / hidden / shown),
so a failure never prints the React node.
happy-dom has no Notification. The tests stubbed it and restored
`window.Notification = undefined`, which `'Notification' in window`
reads as support. A read still in flight after the unmount then reached
Notification.permission in notify.ts: an unhandled error between tests
in the DOM --isolate step on Bun 1.4.4 (tour.test.tsx, 2 runs in 3;
never on 1.3.14). The property is now deleted when there was none
(tour, notifyHook, firstRunNotifications).
…onstructor

`'Notification' in window` counted a property left undefined as
support, so a list read that landed after a test's teardown reached
Notification.permission (the 1.4.4 unhandled error in tour.test.tsx).
Browsers are unaffected (the constructor is always there); the check
now asks for the constructor itself. With the test-side restore in
19b1976, tour.test.tsx runs clean on 1.4.4.
Bun 1.4 stores a `type: "text"` import that is not pure ASCII as its
own embedded file in UTF-16. The three pages (index, review, inbox
.html, ~64 MB together) carry curly quotes and other non-ASCII text,
so they took 128 MB: the darwin-arm64 CLI grew 158 -> 243 MB on 1.4.4
(no sourcemap, bytecode or duplicate; --minify saves 3 MB).

They are now imported `with { type: "file" }` (stored as built) and
read into the same string when a server first needs one
(embeddedPage), so every server gets what it got before and serves
the same bytes and headers. CLI sizes (darwin-arm64 / darwin-x64 /
linux-x64 / linux-arm64 / win-x64 / win-arm64, MB): 179 / 187 / 198 /
198 / 205 / 190, was 243 / 251 / 262 / 262 / 268 / 253 (1.3.14: 158 /
163 / 188 / 188 / 192 / 189). Escaping the pages to pure ASCII and
keeping `type: "text"` measured 180 MB but changes the served bytes.

The read comes from the embedded files Bun holds from startup, not
the binary's path: with the binary renamed over (install.sh), deleted,
or moved aside (Windows installers), a first read returns the running
binary's own bytes (checked on macOS and Linux; smoke-binaries checks
all three OSes).
scripts/smoke-pages.mjs, on macOS, Linux and Windows:
- review and annotate answer `/` with the built review.html and
  index.html byte for byte, with only `Content-Type: text/html`;
  remote mode's gzip answer decompresses to the same bytes; the
  diagram engine files are served as built (digests from the build
  job's new page-digests artifact, never a release asset);
- annotate runs from an installed copy; the copy is replaced the way
  this OS's installer does it, and only then is `/` fetched: still
  the built page;
- a probe compiled on the runner (setup-bun 1.4.4) embeds a file, is
  replaced the same way while running, and only then reads that file
  for the first time: its own bytes, through Bun.file and node:fs.
A swapped digest fails five checks (checked locally).
@backnotprop
backnotprop merged commit 9f3919e into main Oct 27, 2026
32 checks passed
backnotprop added a commit that referenced this pull request Oct 27, 2026
guide-viewer-manifest.ts conflicted (#2215 re-pinned it; this branch did too).
Regenerated from the merged tree, not hand-merged: a clean
`bun install --frozen-lockfile` on Bun 1.4.4, then
`bun run --cwd apps/guides-show build:viewer && bun run --cwd apps/guides-show sync:manifest`.
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.

1 participant