Skip to content

fix(ci,runtime): stop hiding the Bun runtime crash and pin back to 1.4.0 - #4821

Merged
lidge-jun merged 7 commits into
devfrom
codex/win-shard5-bun-crash
Sep 16, 2026
Merged

lidge-jun merged 7 commits into
devfrom
codex/win-shard5-bun-crash

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

The test suite has been crashing the Bun runtime on every CI run since 2026-09-08 and reporting green. This fixes the crash, stops the reporting from hiding it, and repairs the two Windows failures that were underneath.

The crash is a Bun 1.4.2 regression, not a test

The Windows shard emitted a bun.report link that symbolicates into Bun's own module loader: the test runner re-loads the bunfig preload once per file under isolation, and the fault lands in that re-load, before the file's module is evaluated. That is why no test of the named file ever printed a result. Three observations rule out any file being the cause: the shard crashed at the same position on three candidates while the file at that position changed; a file that moved to another shard did not take the crash with it; and on Linux the same fault lands at five unrelated batch boundaries whose frontier files share no import, with every file passing alone.

The bisect lands on the dependency bump of 2026-09-08 that moved dependencies.bun from 1.4.0 to 1.4.2, with CI runs forty minutes either side of it showing the change. This reverts only the version-bearing hunks. 1.4.1 is untested here and the pin is not a claim about it.

The runner was guaranteed to report nothing

On a runtime crash the batch runner re-ran the batch one file per process and returned success. That is not a retry of a flaky test: one preload load never reaches the second that faults, so the sweep was a configuration in which the defect cannot occur. Linux CI has been segfaulting twelve to fourteen times per run and passing.

The sweep is kept for attribution and its verdict changes: a crash records its batch, lets the shard finish so one run attributes every crash site rather than only the first, and exits non-zero with an error annotation. A sweep that finds a genuinely failing file still reports that file. A timeout is untouched, because a timeout is a load condition.

Four inline signature lists collapse into one shared file used by the Linux batch runner and all three workflow legs, so Windows and macOS gain the fatal-signal arm they never had. Exit 3 is classified through the panic banner rather than the bare status, deliberately: unlike 128+N it is an ordinary small exit code, and trusting it bare would reclassify a real failure as a crash and hide it the other way.

The two failures underneath

Windows cold start and a credential race were being charged to two assertions that were not about them, and an explicitly disabled Codex startup was taking the catalog lock.

Verification

  • Root cause traced from the symbolicated stack to the dependency bump, with CI history either side of it as the A/B.
  • Classifier contracts owned by a new test; its assertions were checked mechanically against the real files.
  • A lane=all dispatch on this branch is the only thing that can prove the pin, because an ordinary run cannot while the sweep masks. That dispatch is the gate for this PR.
  • No local suite, focused test, typecheck, build, install, or service command was run.

Checklist

  • Targets dev.
  • Dependency change is a revert to a version with direct CI evidence, not a new adoption.
  • CI reporting becomes stricter rather than more permissive.
  • Green lane=all dispatch with all six Windows shards passing on their own.
  • Maintainer review is complete.

Summary by CodeRabbit

  • Bug Fixes

    • Startup cache invalidation is now skipped when Codex startup synchronization is explicitly disabled.
    • Bun runtime compatibility is pinned to version 1.4.0 to avoid known test-runner crashes.
    • CI now consistently identifies Bun runtime crashes and reports affected test batches as failures instead of recovering them as successful runs.
  • Documentation

    • Updated lifecycle documentation to clarify cache behavior when Codex synchronization is disabled.

Bun re-loads the bunfig preload for every test file under --isolate
(reload_entry_point_for_test_runner -> load_preloads), so a 219-file
Windows shard evaluates tests/preload.ts 219 times in one process.
#4796 added 'import { afterAll } from "bun:test"' to that graph to move
temp-root cleanup onto the test lifecycle. On the 67th re-load Bun
dereferenced a dead JSPromise in its own module loader and took SIGSEGV
at 0x10, inside load_preloads and before the file's first test ran.

Windows shard 5/6 failed that way on both attempts of dispatch runs
35087572377 (dc9d1fa) and 35093667426 (89bdf5f). Linux shards took
the same fault as exit 139 at five unrelated batch boundaries, where the
batch runner hid it by re-running each file alone: one preload load never
reaches the second that faults. Run 35054231781 at 8a1b010 ran the
same files in the same order, without the bun:test edge, and was green.

Cleanup returns to the process exit listener, now on a 250ms removal
budget because there is one listener per test file and the 15s default
that a foreground teardown uses would let a contended exit outlive the
job timeout. The containment and reclamation halves of #4796 are
untouched, and reclamation does not depend on a handler firing.

tests/ci-workflows/test-preload-module-graph.test.ts walks the preload's
load-time graph, fails on any bun:test edge with the chain that
introduced it, and is driven red by a fixture one hop away.
… is fixed

Bun 1.4.2 segfaults while re-loading the bunfig preload. Under --isolate
Bun re-enters tests/preload.ts once per test file, and the fault is in
that path: load_preloads -> wait_for_promise -> drainMicrotasks ->
moduleLoadTopSettled -> JSModuleLoader::loadModule ->
JSPromise::performPromiseThenWithInternalMicrotask -> JSPromise::status,
dereferencing a dead JSCell. Symbolicated from the bun.report link the
Windows shard printed. It is a crash in the interpreter; no test content
avoids it, and it needs at least two preload loads in one process.

A/B, two Linux CI runs 40 minutes apart either side of #4064
(02bc10e), the commit that moved the pin to 1.4.2:
  34274464811  6bd3274  bun 1.4.0   0 panics / 4 shards
  34278407438  02bc10e  bun 1.4.2  12 panics / 4 shards
Also clean on 1.4.0 at ff524ae (34273011879); crashing on 1.4.2 at
365e1ca, db8996d and every sampled tree from 2026-09-10 onward.

Windows shard 5/6 failed on both attempts of runs 35087572377,
35093667426 and 35098735960, always at the 67th file. The file at that
position changed from codex-prompt-layers.test.ts to
codex-prompt-layers-read.test.ts between candidates, and moving
codex-prompt-layers.test.ts into shard 1 left shard 1 green, so the crash
is bound to the preload re-load count and not to any test file.

Reverses only the version-bearing hunks of 02bc10e: package.json,
bun.lock, Dockerfile, and the install-scripts source oracle that pins
both package.json values. The unrelated workflow catch-up in that commit
is left alone.

1.4.1 is untested. This pin is a claim about 1.4.0, which has direct CI
evidence on seven trees, and about 1.4.2, which crashes on every tree
sampled. It is not a claim that 1.4.1 is affected: no run in this
repository ever used it. Whoever moves this line next should treat 1.4.1
as an open question worth checking, not as known-bad, and should prove
any move with a green lane=all dispatch rather than an ordinary PR run,
because the Linux batch sweep hides this class.
… classifier

Linux CI had been segfaulting twelve to fourteen times per run since
2026-09-08 and reporting green every time. run-bun-test-batches.sh
classified the crash, re-ran the batch one file per process, and returned
success when that sweep passed. The sweep was never a retry of a flaky
test: one file per process is a configuration in which this class of
defect cannot occur, so it was guaranteed to pass and guaranteed to
report nothing. That is why a Bun 1.4.2 regression reached a release
candidate with only the Windows lane, which has no sweep, showing it.

The sweep is kept, because proving every file passes alone is the useful
half: it says the defect is in multi-file process state, not in a test.
What changes is the verdict. A runtime crash now records its batch,
finishes the shard so one run attributes every crash site rather than the
first, and exits non-zero with an error annotation. A sweep that finds a
genuinely failing file still reports that file immediately. A timeout is
unchanged and may still recover, because a timeout is a load condition.

The classifier itself was four copies: one in the batch runner and three
inline in ci.yml. ci-workflows.test.ts pinned them in sync, which only
detects the drift it was written to expect - #2152 had already broken one
copy by anchoring on panic(thread 2852) when Bun also emits
panic(main thread). They collapse into scripts/ci/bun-crash-signatures.sh,
which every lane sources. The Windows and macOS legs gain the 132-139
status arm they never had, so a silent fatal signal with no banner is now
caught there too.

Exit 3 is covered through the signature arm rather than the status list.
It is what Windows Bun returns alongside a SIGSEGV panic, but unlike
128+N it is an ordinary small exit code any process may return, and
trusting it bare would reclassify a real failure as a crash and hide it -
the same mistake in the opposite direction. The banner corroborates it.

tests/ci-workflows/ci-bun-crash-classifier.test.ts owns both contracts.
The macOS lane harness now copies the real classifier into its sandbox
rather than a stand-in, and its signature cases exit 3 instead of 139 so
the signature arm is what they actually exercise - with 139 the status
arm matched first and those cases would have passed against an empty
list.
…two assertions

Two unrelated Windows failures, both of which blocked a green lane=all
dispatch just as hard as the Bun crash did.

client-injection-guard: the first child-spawning scenario in the file paid
a one-time cost and was billed for it. Run 35098735960 spent 30.446s in
"deny" and hit the 30s deadline, then ran its siblings in 17.038s and
1.3-2.0s; run 35093667426 paid 3.814s first against 1.6-2.7s later. The
deadline is a guard-behaviour budget, so first-touch transpilation, SQLite
creation and Windows ACL work do not belong inside it. A beforeAll now
walks the real path once and throws the result away. It swallows its own
errors on purpose: the hook asserts nothing, and letting it throw would
turn one broken scenario into seven failures pointing at a warm-up.

server-search: the exact-search 429 case did not receive a 429 at all. It
received a local 401, which the fake upstream cannot produce - it only
ever returns 429 - so the request never reached the fixture. Credential
resolution failed first. The case was starting a whole server between
writing the fixture credential store and reading it, widening the window
in which the process-wide OPENCODEX_HOME can move underneath it, and that
startup is unrelated to the routing contract being asserted. It now calls
handleSearch directly, which is what every other case in this file
already does. Account selection, the public selector, cooldown state and
the no-failover assertion are unchanged; the request-log lookup becomes a
direct read of the log context the handler fills.

Neither is a raised limit. The 30s guard deadline and the 45s composed
watchdog both stay exactly where they were.

The "fixture upstream connection reset" and the compact JSON parse error
in the same job belong to server-auth.test.ts and a malformed-body case
that both passed; they are log noise, not this failure.
…alog lock

Windows shard 4/6 of run 35093667426 failed the WP13 A-reduced composed
acceptance case after 47,774 ms with "timed out waiting for runtime-port
record; child exit=null" and both child pipes still open. Healthy
controls ran the same case in 13,962 ms and 13,718 ms, so the child spent
33.8 s somewhere between spawn and publishing runtime-port.json.

That number has an owner. Startup cache invalidation took catalog lock K
before it checked desired state, and K resolves the effective SID and
LocalAppData through PowerShell, each on a 30,000 ms budget
(src/codex/user-identity.ts, src/lib/windows-user-principal.ts). Results
are cached per process only after they succeed, so a cold resolution that
runs out its budget under six concurrent shards is paid in full. The case
that failed is the one where Codex is explicitly OFF, which is precisely
the case that had no reason to take K at all.

An explicit OFF now short-circuits before the lock. The permit callback
still re-reads intent under K, so a disable racing an enabled startup is
unchanged, and enabled startup behaviour is untouched. Absence still
means ON, so only an explicit false takes the new path.

The composed fixture gains the assertion that an OFF startup creates no
catalog-lock database, which is red against the previous code because the
database was opened before the callback could look at intent. It also
gains incremental stdout/stderr capture and PID/runtime-record
diagnostics: the old failure printed "<stdout still open>", which proved
only that EOF had not arrived and not that the child had said nothing. If
another pre-record path stalls, the next log distinguishes pre-PID,
post-PID, malformed-record and post-runtime health failures instead of
reporting one bare timeout.

The 45 s watchdog is unchanged, and src/server/index.ts stays exactly at
its 893-line ratchet.
@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 86f2e261-3113-4b8e-b412-2e8090ec7fef

📥 Commits

Reviewing files that changed from the base of the PR and between d2808c0 and aa6205f.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .github/workflows/ci.yml
  • Dockerfile
  • package.json
  • scripts/ci/bun-crash-signatures.sh
  • scripts/ci/run-bun-test-batches.sh
  • scripts/test-layout/layout.json
  • src/server/index.ts
  • structure/runtime.md
  • tests/ci-workflows/ci-bun-crash-classifier.test.ts
  • tests/ci-workflows/ci-workflows.test.ts
  • tests/ci-workflows/install-scripts.test.ts
  • tests/ci-workflows/macos-serial-lanes.test.ts
  • tests/codex-integration/client-injection-guard.test.ts
  • tests/codex-integration/codex-composed-acceptance.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/server/server-management-auth.test.ts
  • tests/server/server-search.test.ts

📝 Walkthrough

Walkthrough

Changes

Bun runtime handling

Layer / File(s) Summary
Shared Bun crash classifier
scripts/ci/bun-crash-signatures.sh
Adds guarded shared crash-signature matching, fatal exit-code matching, and the narrow epoll failure rule.
CI integration and shard failure handling
.github/workflows/ci.yml, scripts/ci/run-bun-test-batches.sh
CI lanes source the shared classifier. Linux batches attribute runtime crashes but fail the shard.
Bun version pinning and classifier validation
Dockerfile, package.json, tests/ci-workflows/*, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json
Pins Bun and its type package to 1.4.0. Tests validate classifier ownership, retry behavior, status handling, and shard failure behavior.

Codex startup synchronization

Layer / File(s) Summary
Startup sync guard and lifecycle documentation
src/server/index.ts, structure/runtime.md
Startup cache invalidation and catalog-lock resolution now require enabled Codex startup synchronization.
Acceptance fixture locks and startup diagnostics
tests/codex-integration/codex-composed-acceptance.test.ts
Acceptance fixtures track catalog locks and capture child-stream state and startup diagnostics.

Integration test stability

Layer / File(s) Summary
Client injection fixture cleanup
tests/codex-integration/client-injection-guard.test.ts
Adds configurable child deadlines, shared cleanup, and a deny-mode warm-up.
Management-auth file cleanup
tests/server/server-management-auth.test.ts
Teardown waits for config-directory hardening operations before deleting temporary files.
Direct server search test path
tests/server/server-search.test.ts
The exact-search test calls the handler directly and asserts on its log context.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant CIWorkflow
  participant BunTestBatch
  participant BunCrashClassifier
  participant BunRuntime
  CIWorkflow->>BunTestBatch: run test batch
  BunTestBatch->>BunRuntime: execute tests
  BunRuntime-->>BunTestBatch: status and log
  BunTestBatch->>BunCrashClassifier: classify status and log
  BunCrashClassifier-->>BunTestBatch: runtime crash or other failure
  BunTestBatch-->>CIWorkflow: retry, attribute, or fail shard
Loading

Suggested reviewers: invalid-email-address

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/win-shard5-bun-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 16, 2026
…uth home

Windows shard 1/6 of run 35108652486 threw out of this file's afterEach:

  error: EPERM: operation not permitted, rm
    ...\Temp\opencodex-test-WXhR6c\tmp\ocx-management-auth-fDchUb
      at removeTestTempTree (scripts/test-temp.ts:198)
      at removeTreeWithRetry (tests/helpers/remove-tree.ts:27)
      at tests/server/server-management-auth.test.ts:206

It is a leaked handle, not a timing race, and the budget was never the
answer. hardenConfigDir() spawns icacls.exe, which holds the directory
open until it exits, and Windows file locking is mandatory. #4789 filed
this exact failure at this exact line when the retry budget was 2.5s;
#4796 raised it to a 15s exponential schedule, and this run exhausted all
15 seconds and still got EPERM. Six times the wait changed nothing,
because the handle does not close on the remover's schedule. The process
that started the child has to wait for it.

src/config/paths.ts already documents this and already ships the wait.
The afterEach now awaits flushConfigDirHardeningForTests() first. Twenty
other test files do the same; this one starts servers and drives the ACL
path hardest and did not.

The all-directories variant is the required one. server.stop already
flushes, but through flushConfigDirHardening(), which defaults to
getConfigDir() read at stop time - and this hook restores OPENCODEX_HOME
to the real home a few lines before the removal, so a directory-scoped
flush would settle the wrong tree and leave this one held.

Three things deliberately unchanged. The 15s budget stays: a wider retry
would be treating the symptom, and this run proves it does not work
anyway. The fake icacls runners this file installs are all synchronous,
so awaiting the flush cannot hang on one. And removeTestTempTree keeps
throwing rather than reporting and continuing - that throw is what
surfaced the leak, and softening it would re-hide exactly the class of
defect the batch-runner change stopped hiding this morning.

79 other test files start a server, remove a tree in a cleanup hook, and
never flush. None has failed this way, because this is the file that
drives icacls deliberately, so they are left alone rather than edited
blind. The general guard belongs in the removal helper and is a separate
change.
@lidge-jun
lidge-jun marked this pull request as ready for review September 16, 2026 15:49
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 16, 2026 15:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T15:54:13.266523Z aa6205f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lidge-jun
lidge-jun merged commit 2b19983 into dev Sep 16, 2026
55 of 58 checks passed
@lidge-jun
lidge-jun deleted the codex/win-shard5-bun-crash branch September 16, 2026 15:49
agentHits pushed a commit to agentHits/opencodex that referenced this pull request Sep 17, 2026
…4.0 (lidge-jun#4821)

Release-blocker fix for 2.57.0. The PR aggregate ci check is green at this exact head, and the lane=all dispatch 35113947092 on the same head passed all six Windows shards individually plus both macOS shards, every test shard, gates, keyring, docker and npm-global; only the optional macos control job was cancelled by concurrency. That dispatch is the evidence the Bun pin works: windows 5/6 had failed three release candidates in a row and 1/6 failed the previous dispatch, and all six pass here with the crash masking removed. The suite had been crashing the Bun runtime on every run since 2026-09-08 while reporting green, because a crashed batch was re-run one file per process, which is a configuration in which the defect cannot occur rather than a retry. That verdict now fails. Host-owned merge decision; no local suite, typecheck, build, or install was run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant