Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 31 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe PR fixes iterator cleanup ordering and adds Bun tests. It extends the memory benchmark with reasoning mode, multi-metric post-GC plateau evaluation, checkpoint validation, accumulator integration tests, and updated sandbox memory-limit documentation. ChangesStream cleanup lifecycle
Reasoning memory benchmark
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
WalkthroughBefore this PR, when a turn exited early or a stream timed out, New Features
Bug Fixes
Tests
Documentation
Changes
Magnitude🎯 1 (S) Related
Walkthrough generated by LLxprt PR Review. Planner issue: #2256 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
scripts/issue-2852-memory-benchmark.ts (1)
172-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the metric name to failures raised by
evaluatePostGcPlateau.
evaluatePostGcPlateauthrows'Post-GC heap readings must be positive'when the settled baseline is not positive.externalBytesandwebkitMallocDirtyBytescan read as0in a valid checkpoint, andrequireMetricinscripts/issue-2852-memory-runner.tsaccepts0as a finite value. The runner then fails with a heap-specific message for a non-heap metric. Wrap the call so the error names the failing metric.♻️ Proposed refactor to attach the metric name
const metricResults = extractors.map(({ name, read }) => { const series = samples.map(read); - const result = evaluatePostGcPlateau(series, tolerance); - return { name, ...result }; + try { + return { name, ...evaluatePostGcPlateau(series, tolerance) }; + } catch (error) { + throw new Error(`Metric ${name}: ${(error as Error).message}`); + } });🤖 Prompt for 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. In `@scripts/issue-2852-memory-benchmark.ts` around lines 172 - 176, Update the metric evaluation in the extractor map around evaluatePostGcPlateau so thrown errors are caught and rethrown with the current metric name included, while preserving the original error details and successful results unchanged.
🤖 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/agents/src/core/turn.cooperative-cleanup.bun.test.ts`:
- Around line 178-208: In the noncooperative iterator test, tighten the
elapsed-time assertion after turn.run completes to a CI-tolerant threshold close
to the documented one-second cleanup timeout, rather than allowing up to five
seconds. Keep the existing event and returnCalled assertions unchanged.
In `@scripts/tests/issue-2852-memory-plateau.bun.test.ts`:
- Around line 114-126: Update the test around evaluateMultiMetricPlateau to
assert the heap metric’s growthRatio in addition to settledBaselineBytes and
withinTolerance. Keep the existing scenario and assertions, and use the expected
ratio for the 40M-to-55M input.
---
Nitpick comments:
In `@scripts/issue-2852-memory-benchmark.ts`:
- Around line 172-176: Update the metric evaluation in the extractor map around
evaluatePostGcPlateau so thrown errors are caught and rethrown with the current
metric name included, while preserving the original error details and successful
results unchanged.
🪄 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: 9e866e27-9da9-4251-a650-b70faa36d609
⛔ Files ignored due to path filters (1)
project-plans/issue3114-memory-plateau.mdis excluded by!project-plans/**
📒 Files selected for processing (7)
docs/sandbox.mdpackages/agents/src/core/turn.cooperative-cleanup.bun.test.tspackages/agents/src/core/turn.tsscripts/issue-2852-memory-benchmark.tsscripts/issue-2852-memory-runner.tsscripts/issue-2852-memory-target.tsscripts/tests/issue-2852-memory-plateau.bun.test.ts
OpenCodeReview — automatic reviews suspendedAutomatic OCR reviews are suspended for this PR after 2 of 2 automatic reviews. To get more reviews you can:
|
|
Fixed in e9c923a. Agreed — a 5s allowance against a documented 1s bound would have let a regression that adds another whole second to every turn pass unnoticed, which is exactly the class of regression this test exists to catch. Tightened to 2.5s: still enough slack for scheduling jitter on a loaded CI runner, but it now fails on any additional second of cleanup latency. The event and returnCalled assertions are unchanged. |
There was a problem hiding this comment.
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 `@scripts/issue-2852-memory-benchmark.ts`:
- Line 119: Treat whitespace-only JSONL lines as blank by updating the line
filter in scripts/issue-2852-memory-benchmark.ts (119-119) to trim before
checking length. Add a whitespace-only input and assertion that it is ignored in
scripts/tests/issue-2852-memory-plateau.bun.test.ts (133-146).
🪄 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: 140e032f-546f-4f98-8822-19bbd4f3e0f0
📒 Files selected for processing (4)
packages/agents/src/core/turn.cooperative-cleanup.bun.test.tsscripts/issue-2852-memory-benchmark.tsscripts/issue-2852-memory-runner.tsscripts/tests/issue-2852-memory-plateau.bun.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/agents/src/core/turn.cooperative-cleanup.bun.test.ts
- scripts/issue-2852-memory-runner.ts
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.
…eckpoints 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.
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.
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.
TLDR
Long-running sessions grew memory with uptime, not activity — a 128 GiB machine was driven to 64 MB free and 98.5% swap exhaustion, with the two worst offenders being the two most idle sessions.
Most of umbrella issue #3114 has already landed as separate PRs. This lands the one attributed retention defect still outstanding, plus the measurement that proves the combined result actually holds.
The bug:
Turn.cleanupStreamResourcesaborted the timeout controller before callingcloseIteratorBounded. That helper early-returns when the signal it is handed is already aborted — so the abort raced ahead of its own cleanup and the branch that awaitsiterator.return()was unreachable on every single turn. Cooperative provider iterators never finished unwinding, so the generator scope capturing each turn's stream state stayed alive.Reviewers should look at: the ordering change in
turn.ts(it is three lines and the whole fix), and whether the reasoning benchmark is a fair proxy for the real leak.Dive Deeper
Already merged elsewhere — deliberately not re-implemented here
StreamOutputAccumulator— the primary retention site--max-old-space-sizesuppressionThis branch was rebased onto those. An earlier revision of this PR independently implemented the #3112 footer/bootstrap work before #3123 merged; that work was dropped in favour of the merged version rather than re-landed.
The iterator cleanup fix
Closure is now awaited first; the controller is aborted after.
The cleanup signal is omitted deliberately — passing the turn-owned signal is precisely 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.
onParentAbortalready 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.This is a fail-fast fix at the attributed site, not a defensive guard: nothing was wrapped, no error is swallowed that was not already warning-only, and the existing bound is preserved rather than duplicated.
Why a multi-metric plateau gate
The existing harness gated only on post-GC JSC heap. That would have declared victory here while the strings were still resident: the
vmmapinvestigation attributed 32.9 GB toexternalagainst 0.5 GB of ArrayBuffers, and under Bunexternaltracks JS string backing stores at roughly a byte per character.The gate now evaluates JSC heap,
process.memoryUsage().external, and dirty WebKit Malloc independently and passes only if all three settle.evaluateMultiMetricPlateaureuses the existing per-metric evaluator and warm-up handling rather than introducing a second notion of "plateau".Why the reasoning workload looks the way it does
reasoningmode drives the realStreamOutputAccumulator— not a stand-in — with 200 full-so-far thinking deltas per turn against a 30 KB final thought, which is the shape Anthropic actually streams.Two details that matter for it being a real measurement:
Bufferrather than produced byString.prototype.slice, so the workload allocates distinct string backing stores instead of engine-dependent substring views that would understate retention.This extends
scripts/issue-2852-memory-runner.ts; no parallel harness was added.No silent data loss
The reasoning span is retained in full. Only the duplicate partial copies of it are not. No new length, count, or disk bound is introduced, so no truncation label is required.
Documentation
docs/sandbox.mdstill described the Node heap limit as automatically derived regardless of runtime, which #3123 made untrue when it stopped passing--max-old-space-sizeunder Bun. Corrected so the docs match shipped behaviour.Reviewer Test Plan
1. Confirm the cleanup tests are real (RED/GREEN), not mock theater.
Then revert just the fix and re-run:
Expect the two cooperative cases to fail with
Expected: false, Received: true— the turn finishing before cleanup released. The bounded-timeout case still passes, confirming the existing timeout behaviour is untouched. Restore afterwards.These tests drive the public
Turn.run()with a provider iterator whosereturn()settles only on a controlled release; they assert observable turn completion ordering, not that a mock was called.2. Run the real memory benchmark (macOS; uses
vmmap/footprint).Exit 0 and
overallWithinTolerance: true. Observed on this branch:To see the gate actually bite, hand-edit one post-GC
externalvalue upward in the target JSONL and re-derive — the run fails naming the offending metric.3. Confirm the pre-existing text/media modes still work.
4. Long-session sanity. Run an interactive session against a reasoning-capable model (Anthropic or an OpenAI Responses model), leave it idle across many turns, and watch the footer RSS/External. Growth should track activity, not uptime.
Testing Matrix
Verified on macOS 26.4 arm64 / Bun 1.3.14:
npm run test— exit 0, zero failures across all 17 workspaces (agents 350/350, CLI 681/681, core 546/546)npm run lint— exit 0npm run typecheck— exit 0npm run format— cleannpm run build— exit 0bun scripts/start.ts --profile-load stepfun-37 "write me a haiku and nothing else"— exit 0bun scripts/tmux-harness.ts— exit 0The benchmark is macOS-specific by design (it shells out to
vmmapandfootprint), as was the pre-existing issue-2852 harness it extends. It is a developer tool, not part of CI.A note on agents-suite flakiness
While validating, the agents suite intermittently failed 1–4 files on timeouts only, with a different set each run. To rule out this change as the cause, the suite was run against the unmodified
turn.ts: that baseline failed more files (displayCallbacks,memoryControl), which reproduces the flake independently of this PR. Every implicated file passes in isolation. The final clean run on this head is fully green.Linked issues / bugs
Fixes #3114
Depends on and builds atop #3117, #3123, #3125 and #3124, all merged.
Summary by CodeRabbit
Bug Fixes
Documentation
Diagnostics