Skip to content

Stop reporting Bun's nonexistent heap ceiling (Fixes #3112) - #3123

Merged
acoliver merged 1 commit into
mainfrom
issue3112
Aug 7, 2026
Merged

Stop reporting Bun's nonexistent heap ceiling (Fixes #3112)#3123
acoliver merged 1 commit into
mainfrom
issue3112

Conversation

@acoliver

@acoliver acoliver commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

TLDR

Stop treating Bun's compatibility V8 heap statistic as a real memory ceiling. Bun now shows current heap usage without a fabricated denominator and skips both host and sandbox relaunch arguments that Bun ignores. The meaningful Node behavior remains unchanged, including a live heap-limit denominator that is refreshed rather than memoized.

Dive Deeper

  • Detect Bun using the established non-empty process.versions.bun convention at the existing CLI decision points.
  • Render Bun memory as used heap plus RSS, retaining External and ArrayBuffers in the wide footer without implying a hard ceiling.
  • Preserve the Node denominator while reading the current heap statistic for each periodic snapshot.
  • Return no max-old-space-size arguments for Bun host relaunches or Bun-fronted sandbox launches before OS or V8 calculations.
  • Keep all Node cap, floor, rounding, debug, and no-relaunch behavior intact.
  • Audit remaining heap-flag uses and leave Node-fronted package, lint, and CI settings unchanged.
  • Document the accepted behavior and preflight classification in project-plans/issue3112.

Review findings were classified as Blocker-Fix, In-scope-Fix, Reject, or Defer. DeepThinker and local Open Code Review completed; all valid in-scope findings were resolved. No public API, dependency, workflow, lint-rule, or memory-retention change was introduced.

Reviewer Test Plan

  1. Run the focused behavioral tests:

    cd packages/cli
    bun test --timeout 30000 ./src/ui/components/Footer.test.tsx ./src/utils/bootstrap.test.ts
    

    Expected: 82 tests pass.

  2. Start the CLI with memory display enabled under Bun and confirm the footer resembles:

    Heap: 0.2GB RSS: 0.4GB
    

    It must not contain a slash denominator. At wide width it should also show External and ArrayBuffers.

  3. Review the Node-like test cases to confirm the heap denominator remains and changes after the two-second refresh when the mocked V8 statistic changes.

  4. Review the bootstrap Bun cases to confirm both memory-argument functions return an empty array before host/V8 calculations, while the inherited Node calculation cases remain green.

Local evidence on macOS:

  • Focused issue tests: 82 passed, 0 failed.
  • Direct ESLint on all changed TypeScript/TSX files: passed with zero warnings.
  • ESLint guard: passed.
  • Typecheck: passed after the dependency-ordered build.
  • Build: passed.
  • Format: passed.
  • StepFun smoke test: passed with a valid haiku.
  • Tmux harness: passed; raw output showed Heap: 0.2GB RSS: 0.4GB with no denominator.
  • Isolated agents profiles suite: 22 passed, 0 failed.
  • Isolated agents hooks suite: 17 passed, 0 failed.

Repository-wide local caveats, reported without weakening gates or expanding this issue:

  • npm run test completed with exit 1 because the agents workspace runner labeled hooks.spec.ts failed despite that file's captured output and isolated run showing 17 passed and 0 failed. A prior run similarly labeled profiles.spec.ts failed despite 22 passed and 0 failed in isolation.
  • npm run lint completed with 231 errors in unchanged existing Bun-migration test files. Every changed file passes direct ESLint, and the lint guard passes. CI remains authoritative and will be watched to completion.

Testing Matrix

🍏 🪟 🐧
npm run ✅ focused/build/typecheck/format/smoke; full test/lint caveats above
npx ✅ changed-file ESLint
Docker
Podman - -
Seatbelt ✅ tmux harness - -

Linked issues / bugs

Fixes #3112

Related to #3108

Summary by CodeRabbit

  • Bug Fixes

    • Improved memory usage displays when running under Bun by removing unsupported heap-limit values.
    • Updated memory management behavior to avoid applying Node-specific heap arguments in Bun environments.
    • Refreshed heap-limit information on demand for more accurate memory reporting.
    • Improved memory and RSS updates after automatic refresh intervals.
  • Documentation

    • Clarified runtime-specific memory configuration and reporting behavior.

@github-actions github-actions Bot added the maintainer:e2e:ok Trusted contributor; maintainer-approved E2E run label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI now detects Bun and skips ineffective Node heap arguments. The footer omits Bun heap-limit denominators and refreshes Node heap limits on each call. Tests use Bun-compatible utilities and cover runtime-specific memory behavior.

Changes

Bun memory behavior

Layer / File(s) Summary
Runtime-aware memory argument handling
packages/cli/src/utils/bootstrap.ts, packages/cli/src/cliSandbox.ts
Bun detection makes startup and sandbox memory helpers return empty argument lists. Documentation describes the separate Bun and Node behavior.
Runtime-aware footer display
packages/cli/src/ui/components/Footer.tsx
The footer omits the heap-limit denominator under Bun. Node heap limits are read on each invocation.
Bun and refresh behavior coverage
packages/cli/src/ui/components/Footer.test.tsx, packages/cli/src/utils/bootstrap.test.ts
Tests migrate to Bun-compatible utilities and cover Bun memory displays, refreshed metrics, Node heap-limit updates, and empty Bun memory arguments.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • Issue 3114 — Covers the same Bun heap-reporting and ineffective Node heap-argument behavior.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: removing Bun's misleading heap ceiling reporting.
Description check ✅ Passed The description covers the required sections, implementation scope, testing plan, matrix, linked issues, and known test caveats.
Linked Issues check ✅ Passed The changes address issue #3112 by removing Bun heap-limit reporting, skipping ineffective memory arguments, refreshing Node limits, and preserving Node behavior.
Out of Scope Changes check ✅ Passed The reviewed changes remain within issue #3112 scope and support Bun memory reporting, relaunch behavior, documentation, and regression testing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue3112

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

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Before this PR, the CLI treated memory/relaunch and footer heap display as Node-only assumptions. In bootstrap, the startup relaunch path unconditionally used v8 heap statistics and os.totalmem() to compute a larger Node heap, so under Bun it could try to relaunch with --max-old-space-size even though Bun does not expose the same Node heap ceiling. In the sandbox path, computeSandboxMemoryArgsFromEnv still flowed through the same Node-centric logic, and the footer always read v8.getHeapStatistics().heap_size_limit to show a denominator like 8.0GB. After this PR, the code detects when it is running under Bun, skips the Node-specific relaunch/sandbox memory-arg computation in those paths, and stops showing a fabricated Bun heap ceiling by omitting the denominator from the footer heap line. The result is that Bun users no longer see bogus relaunch attempts or an inaccurate heap-limit display, while Node behavior remains unchanged.

Release Notes

  • Bug Fixes

    • Stop reporting a nonexistent heap ceiling under Bun by omitting the denominator from the footer heap line.
    • Short-circuit Node-specific heap/memory relaunch logic when running under Bun, avoiding bogus relaunch and sandbox memory-arg computation.
  • Tests

    • Add Bun-specific coverage for memory relaunch behavior and footer heap display so runtime-specific paths are protected.
  • Documentation

    • Add planning/specification documents describing the behavioral requirements and preflight verification for the Bun heap-ceiling fix.

Changes

Layer File(s) Summary
core packages/cli/src/utils/bootstrap.ts, packages/cli/src/cliSandbox.ts Adds Bun runtime detection and short-circuits Node-specific heap/memory relaunch logic in bootstrap and sandbox memory arg computation.
ui packages/cli/src/ui/components/Footer.tsx Stops showing a fabricated heap limit under Bun by detecting the runtime and omitting the denominator from the footer's heap line.
tests packages/cli/src/utils/bootstrap.test.ts, packages/cli/src/ui/components/Footer.test.tsx Migrates tests to bun:test and adds Bun-specific suites verifying memory relaunch and footer heap display behavior.
docs project-plans/issue3112/plan/00a-preflight-verification.md, project-plans/issue3112/specification.md Planning and specification documents defining behavioral requirements and preflight verification for the Bun heap ceiling fix.

Sequence Diagram

sequenceDiagram
  User->>CLI: Run command
  CLI->>Bootstrap: maybeRelaunchForMemory
  Bootstrap->>Bootstrap: Detect Bun runtime
  alt Bun runtime
    Bootstrap-->>CLI: Skip Node heap relaunch
  else Node runtime
    Bootstrap->>CLI: Return memory relaunch args
  end
  CLI->>Sandbox: maybeHopIntoSandbox
  Sandbox->>Bootstrap: computeSandboxMemoryArgsFromEnv
  Bootstrap->>Bootstrap: Detect Bun runtime
  alt Bun runtime
    Bootstrap-->>Sandbox: Return empty memory args
  else Node runtime
    Bootstrap->>Sandbox: Return --max-old-space-size args
  end
  Sandbox->>CLI: Start sandbox process
  CLI->>Footer: Render footer
  Footer->>Footer: Detect Bun runtime
  alt Bun runtime
    Footer->>Footer: Omit heap limit denominator
  else Node runtime
    Footer->>Footer: Include heap limit denominator
  end
Loading

Magnitude

🎯 1 (S)
562 additions, 28 deletions, 7 changed files across 1 package, 0 acceptance criteria

Related

Pre-merge Checks

Check Status Note
Title Clear and descriptive; states the fix and references the issue.
Description Includes all required template sections (TLDR, Dive Deeper, Reviewer Test Plan, Testing Matrix, Linked issues / bugs) with detailed implementation notes and test evidence.
Linked Issues Addresses #3112 by adding Bun runtime detection, removing the fabricated heap-limit denominator from the footer, stopping Bun relaunches with ignored --max-old-space-size flags, and adding behavioral tests. Node behavior is preserved. The underlying retention issue (#3108) is correctly left out of scope.
Out of Scope The actual memory retention/leak investigation and fixes from #3108 (C1-C5 candidate sites) are explicitly out of scope and not addressed. No attempt is made to bound outputChunks, add backpressure, fix iterator cleanup ordering, or rotate error reports. The 'honest fallback' for a real memory ceiling is also deferred to #3108 per the issue's own scope note.

Walkthrough generated by LLxprt PR Review. Planner issue: #2256

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/utils/bootstrap.test.ts`:
- Around line 35-39: Update the afterEach cleanup to remove process.versions.bun
when originalBunDescriptor is undefined, while restoring the saved descriptor
when it exists. Ensure cleanup leaves process.versions matching its state before
each test so later runtime detection is unaffected.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d48d5311-75c6-4a57-8f44-8c573069831c

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae245b and 30e546b.

⛔ Files ignored due to path filters (2)
  • project-plans/issue3112/plan/00a-preflight-verification.md is excluded by !project-plans/**
  • project-plans/issue3112/specification.md is excluded by !project-plans/**
📒 Files selected for processing (5)
  • packages/cli/src/cliSandbox.ts
  • packages/cli/src/ui/components/Footer.test.tsx
  • packages/cli/src/ui/components/Footer.tsx
  • packages/cli/src/utils/bootstrap.test.ts
  • packages/cli/src/utils/bootstrap.ts

Comment thread packages/cli/src/utils/bootstrap.test.ts
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

OpenCodeReview — PR #3123

  • Reviewed head SHA: 30e546b622ccd90762de6a6a5958c6cd328927eb
  • Merge base: 2ae245b8d7d9d5c445480c2d1315598a46dfc604
  • Range: full from 2ae245b8d7d9d5c445480c2d1315598a46dfc604
  • Range fallback: none
  • Scope: selected 7 file(s), +562/-28; cumulative 7 file(s), +562/-28
  • Tokens: 212352 total (154345 input, 58007 output, 85632 cache)
  • OCR version: open-code-review v1.8.4 (e78474478) linux/amd64 built at: 2026-08-01T03:27:37Z https://github.com/alibaba/open-code-review
  • Phase: review
  • Exit code: 0
  • Run: https://github.com/vybestack/llxprt-code/actions/runs/31162534780
  • No findings.
  • Artifacts: ocr-review-output contains raw JSON, stdout, stderr, preview, phase, and exit-code diagnostics.
  • WARNING: Changed-file coverage 0/5 preview files covered is below the 90% threshold.

@acoliver
acoliver merged commit 73dfc7a into main Aug 7, 2026
39 of 40 checks passed
acoliver added a commit that referenced this pull request Aug 7, 2026
Long-running sessions grew memory with uptime rather than activity. Most of
the umbrella issue has now landed as separate PRs: #3117 coalesced thinking
blocks in `StreamOutputAccumulator` (the primary retention site), #3123 made
the Bun memory readout honest, #3125 removed the history deep-clone and
#3124 bounded error reports. This lands the one attributed retention defect
still outstanding, and the measurement that shows the combined result holds.

`Turn.cleanupStreamResources` aborted the timeout controller *before*
calling `closeIteratorBounded`. Because that helper early-returns when the
signal it is handed is already aborted, the abort raced ahead of its own
cleanup and the branch that awaits `iterator.return()` was unreachable on
every turn. A cooperative provider iterator therefore never finished
unwinding, so the generator scope capturing the turn's stream state stayed
alive. Closure is now awaited first and the controller is aborted after.
The cleanup signal is omitted deliberately: passing the turn-owned signal
is what defeated the wait. A noncooperative iterator is still bounded by
`closeIteratorBounded`'s own one-second timeout, so this cannot hang.

There is no added cancellation latency. `onParentAbort` already aborts the
timeout controller from the parent signal, so on user cancel the controller
is aborted before cleanup runs; the reordering only affects normal
completion and early consumer exit. Both new cases fail against the previous
implementation and pass against this one.

Proof extends `scripts/issue-2852-memory-runner.ts` rather than adding a
parallel harness. A new `reasoning` mode drives the real
`StreamOutputAccumulator` with 200 full-so-far thinking deltas per turn
against a 30 KB final thought, the shape Anthropic actually streams. Each
prefix is copied through a Buffer so the workload allocates distinct string
backing stores instead of engine-dependent substring views, and the turn
asserts the span collapses to exactly one block carrying the full final
text, stream id, status and signature -- so the target fails loudly if that
coalescing ever regresses.

The plateau gate now evaluates JSC heap, `process.memoryUsage().external`
and dirty WebKit Malloc from `vmmap` independently, and passes only if all
three settle. External is the metric that matters here: the vmmap
investigation attributed 32.9 GB to it against 0.5 GB of ArrayBuffers, and
under Bun it tracks string backing stores at roughly a byte per character.
Gating on JSC heap alone would have declared victory while the strings were
still resident. Measured over four turns: JSC heap +0.5%, external +2.3%,
dirty WebKit Malloc +0.0%.

`docs/sandbox.md` still described the Node heap limit as automatically
derived regardless of runtime, which #3123 made untrue when it stopped
passing `--max-old-space-size` under Bun. Corrected so the documentation
matches the shipped behavior.

No bound discards content. The reasoning span is retained in full; only the
duplicate partial copies of it are not.
acoliver added a commit that referenced this pull request Aug 8, 2026
…) (#3127)

* Release per-turn stream state and prove the memory plateau (Fixes #3114)

Long-running sessions grew memory with uptime rather than activity. Most of
the umbrella issue has now landed as separate PRs: #3117 coalesced thinking
blocks in `StreamOutputAccumulator` (the primary retention site), #3123 made
the Bun memory readout honest, #3125 removed the history deep-clone and
#3124 bounded error reports. This lands the one attributed retention defect
still outstanding, and the measurement that shows the combined result holds.

`Turn.cleanupStreamResources` aborted the timeout controller *before*
calling `closeIteratorBounded`. Because that helper early-returns when the
signal it is handed is already aborted, the abort raced ahead of its own
cleanup and the branch that awaits `iterator.return()` was unreachable on
every turn. A cooperative provider iterator therefore never finished
unwinding, so the generator scope capturing the turn's stream state stayed
alive. Closure is now awaited first and the controller is aborted after.
The cleanup signal is omitted deliberately: passing the turn-owned signal
is what defeated the wait. A noncooperative iterator is still bounded by
`closeIteratorBounded`'s own one-second timeout, so this cannot hang.

There is no added cancellation latency. `onParentAbort` already aborts the
timeout controller from the parent signal, so on user cancel the controller
is aborted before cleanup runs; the reordering only affects normal
completion and early consumer exit. Both new cases fail against the previous
implementation and pass against this one.

Proof extends `scripts/issue-2852-memory-runner.ts` rather than adding a
parallel harness. A new `reasoning` mode drives the real
`StreamOutputAccumulator` with 200 full-so-far thinking deltas per turn
against a 30 KB final thought, the shape Anthropic actually streams. Each
prefix is copied through a Buffer so the workload allocates distinct string
backing stores instead of engine-dependent substring views, and the turn
asserts the span collapses to exactly one block carrying the full final
text, stream id, status and signature -- so the target fails loudly if that
coalescing ever regresses.

The plateau gate now evaluates JSC heap, `process.memoryUsage().external`
and dirty WebKit Malloc from `vmmap` independently, and passes only if all
three settle. External is the metric that matters here: the vmmap
investigation attributed 32.9 GB to it against 0.5 GB of ArrayBuffers, and
under Bun it tracks string backing stores at roughly a byte per character.
Gating on JSC heap alone would have declared victory while the strings were
still resident. Measured over four turns: JSC heap +0.5%, external +2.3%,
dirty WebKit Malloc +0.0%.

`docs/sandbox.md` still described the Node heap limit as automatically
derived regardless of runtime, which #3123 made untrue when it stopped
passing `--max-old-space-size` under Bun. Corrected so the documentation
matches the shipped behavior.

No bound discards content. The reasoning span is retained in full; only the
duplicate partial copies of it are not.

* Address review: tighten cleanup-bound assertion and name malformed checkpoints

CodeRabbit: the noncooperative-cleanup assertion allowed 5s against a
documented 1s bound, so a regression adding a whole extra second per turn
would have passed. Tightened to 2.5s, which still absorbs CI scheduling
jitter. The plateau test promised a growth-ratio assertion in its name but
only checked the baseline and verdict; growthRatio and maxBytes are now
asserted too.

OCR: a malformed checkpoint line surfaced as a bare SyntaxError naming
neither the file nor the line, across artifacts with hundreds of records.
Both readers now share readPostGcRecords, which reports the offending path
and line number and re-throws — a corrupt artifact still fails the run
rather than being tolerated. Sharing the reader also removes the duplicated
read/split/filter/parse chain, and readPostGcHeapBytes now reuses
requireMetric instead of carrying its own copy of the same check.

* Report the malformed checkpoint line as it appears in the file

The line-number reporting added in the previous commit was wrong: blank
lines were filtered out before the index was taken, so the reported number
counted only non-blank lines. On a file with blank lines it pointed at the
wrong record, which defeats the entire purpose of naming the line.

Line numbers are now captured before blanks are dropped. The parser moved to
issue-2852-memory-benchmark.ts, alongside the other artifact parsers, so it
takes its contents as an argument and can be tested directly; the runner
still owns reading the file. Three behavioral tests cover it, including one
whose malformed record sits on physical line 4 behind two blank lines and
would have been misreported as line 2 by the previous implementation.

* Treat whitespace-only checkpoint lines as blank

A line holding only spaces or a tab is not a record, but it was reaching
JSON.parse and failing the run on an artifact that is structurally fine.
The blank-line filter now trims before measuring, and the test feeds both a
spaces-only and a tab-only line to prove they are ignored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer:e2e:ok Trusted contributor; maintainer-approved E2E run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Footer reports a fabricated heap limit and startup relaunches for a --max-old-space-size that Bun ignores

1 participant