Skip to content

[mem] fix(memory): release consumed buffers, abandoned waits, and deleted diff views - #30650

Merged
nwparker merged 7 commits into
mainfrom
OrcaWin/np-OOMs-10-19
Oct 19, 2026
Merged

nwparker merged 7 commits into
mainfrom
OrcaWin/np-OOMs-10-19

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​535 $\color{#cf222e}{\Huge{\mathbf{−}}}$​23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​512
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​293 $\color{#cf222e}{\Huge{\mathbf{−}}}$​125 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​168

ELI5

Long sessions could keep old terminal output, cancelled requests, paste text and deleted workspaces' diffs alive. Release those objects when their owner is finished so memory can return to the collector.

What Changed

Fix five retention areas, plus the relay's related deadline wait:

  • JSON stream readers retain standalone unfinished text, paused records and error prefixes instead of the entire consumed input chunk.
  • Binary terminal readers copy a small leftover frame fragment once at the input boundary, releasing the consumed buffer.
  • Cancelled callers detach from shared promises through the existing waiter owner. One caller cancelling leaves other callers and the underlying work running.
  • Buffered terminal writes remove their abort listener on success, withdrawal and clear, releasing retained paste text.
  • Combined-diff view memory (file bodies, scroll offset, scroll anchor) is one entry per view, tagged with its workspace. Each mounted viewer writes through a lease; deleting or authoritatively removing a workspace drops its entries and revokes its leases, so the viewer's own unmount snapshot — which React runs after the store update — cannot re-create them. A still-mounted viewer whose entry was evicted by the 20-entry LRU re-creates it on its next write. Ordinary tab switches, another host's same-ID workspace and renames (ownership transfers, bodies and scroll kept) are unaffected. The cache module has no browser side effects, so store teardown can import it.
  • Relay readiness deadlines detach their caller from the pending reconnect operation by reusing waitForPromiseWithSignal.

Why

These are ownership fixes. They preserve content, input order, history, existing cache policies and wire formats; they add no caps or truncation. Copying only surviving fragments avoids retaining consumed data, and using the existing waiter implementation keeps cancellation ownership explicit.

The forced-GC regressions failed against the original production code. On macOS / Node 24, with 32 readers or cancelled callers:

Retention scenario Before After
JSON unfinished tails 32.2 MiB 0.33 MiB
JSON paused records 32.2 MiB 0.18 MiB
JSON diagnostic prefixes 32.0 MiB 2.04 MiB
Binary frame fragments 33.0 MiB 2.0 MiB
Cancelled request payloads, before shared work finishes 32 of 32 retained 0 of 32 retained

An alternating five-sample local microbenchmark processed the same 163,840 records per sample: JSON median 57.05 → 55.59 ms; binary median 56.47 → 56.85 ms. Both stayed within 3% of baseline. Electron's JavaScript runtime independently reproduced the JSON retention drop (32.0–32.2 MiB → 0.15–2.03 MiB); the same throughput samples showed no slowdown there.

Linked Issue

User-requested OOM audit; no existing issue linked.

Visual Proof

No visual change. Validated in a background-launched Electron build of this branch (CDP, isolated profile, 20-file repo):

  • Switching tabs away from a scrolled combined diff and back restores the same position every frame (4500 → 4491, same lines on screen; never 0).
  • Removing a workspace while its diff is open leaves no cache entry, including after the viewer unmounts; recreating a workspace at the same path opens fresh at the top, and later scroll memory works again.
  • No renderer console errors in those flows.

Testing

  • Locally exercised forced GC, repeated cancellation, expired relay waits, terminal input flush/clear, workspace removal, folder workspaces and same-ID host preservation on macOS.
  • Automated regression tests added; reproduced failures with the original production implementations.
  • pnpm tc — passed.
  • pnpm run check:code-quality:changed — passed; commit hooks also passed lint, React Doctor and formatting.
  • pnpm run build:electron-vite — passed, including renderer boot-graph verification.
  • Targeted parser, Claude/Codex/ACP, relay, terminal input and workspace-removal suites — 297 passed, one existing test skipped; additional folder, host-removal and cancellation-order regressions passed. After separating cache storage from browser listener registration, 22 affected caller suites passed (178 tests). Rename/deletion, rename agreement, diff restore and JSON retention regressions also pass (15 tests).
  • Full PR Checks workflow — the verify aggregate (PR checks passed) passed for b49cd7f8db208b66babc644b24962a24630d590a, including all unit shards, relay integration, Linux/Windows packaging and static analysis. Earlier commit b76926dd hit a Codex dispatch test that was red on main at the time (fixed by test(codex): pin the stopped-send receipt rule where #30403 moved it (fix red main) #30865). No native Windows/Linux or live SSH session was exercised locally.

Review

Shared-cancellation ordering trade-off (reviewed independently): all callers waiting on the same promise now settle in the first caller's reaction turn. A later caller can therefore resume one step before an unrelated .then attached between it and the first caller, and an abort fired from such a reaction no longer wins. Exact old ordering needs one reaction per caller, which is the leak. No current caller has that shape (shared-promise sites audited: orcad artifact materializer, Jira image cache, relay auth, PTY dialect probe, primary state writes, worktree preparation).

Risk assessment: low. The full PR workflow passed on the current commit b49cd7f8; independent reviews of the diff-cache lease and shared-cancellation ordering found no blocking issues. The most sensitive change is shared cancellation timing; tests cover cancellation before cached delivery, surviving callers, late failures and releasing stalled callers before the underlying operation finishes. Parser tests verify reconstruction and Unicode behavior. Existing limits and paired-client wire contracts are unchanged. Content invalidation continues to preserve scroll restoration. Scroll-only entries (no saved body) are owned too, so workspace removal releases them as well.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

All changes use existing cross-platform JavaScript/Node facilities. Execution-host authority is unchanged. Deleted-workspace cache cleanup applies to folder workspaces and preserves a same-ID workspace still published by another host.

Checklist

  • This PR is focused on resource ownership; most added lines are regression tests.
  • I explained what changed and why, including the user-facing before/after and mechanism.
  • N/A for visual proof, with reason above.
  • Self-reviewed for correctness, security and performance.
  • Cross-platform, SSH/remote, folder workspace and compatibility impact considered.
  • Full repository checks and packaging pass in CI for the current PR commit; local targeted checks and Electron build passed.

OOM audit PR list

The two follow-ups are independent branches based directly on this PR. Together they address four additional reproduced OOM mechanisms.

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9ac962cc-7508-436e-b831-2e9d066fb6cb
📥 Commits

Reviewing files that changed from the base of the PR and between b76926d and b49cd7f.

📒 Files selected for processing: 16
  • src/main/daemon/daemon-stream-binary-framing.ts
  • src/main/runtime/mobile-cloud-relay/relay-readiness.ts
  • src/renderer/src/components/editor/combined-diff/CombinedDiffViewer.tsx
  • src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-cache.ts
  • src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-memory.ts
  • src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-persist.ts
  • src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.test.tsx
  • src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.ts
  • src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-scaling.test.tsx
  • src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-restore-signal-equivalence.test.tsx
  • src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-scroll-anchors.ts
  • src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-scroll-persistence.ts
  • src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts
  • src/renderer/src/store/slices/removed-worktree-diff-memory.test.ts
  • src/shared/main-process-ndjson-framer.ts
  • src/shared/promise-settlement-waiters.ts
🚧 Files skipped from review as they are similar to previous changes: 1
  • src/shared/promise-settlement-waiters.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

📝 Walkthrough

The changes reduce retained memory in binary stream and NDJSON readers, and update shared-promise settlement handling for abortable waits. Relay readiness uses the shared waiter. Combined-diff view state now records its worktree owner and is renamed or cleared during worktree changes. Accepted terminal inputs remove their abort listeners when resolved. New tests cover memory retention, waiter behavior, cache cleanup, and listener lifecycle.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to b49cd

An externally changed diff may reopen at an outdated scroll position. This is a bounded view-restoration issue that can be addressed before merge or accepted for follow-up.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 35 functions across 27 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title is concise and clearly summarizes the main memory-retention fixes, including consumed buffers, abandoned waits, and deleted diff views.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is detailed and covers the required change summary, rationale, testing, review risks, compatibility notes, visual impact, and checklist. It does not link an actual issue and does not u…
Full details: Description check

Explanation

The description is detailed and covers the required change summary, rationale, testing, review risks, compatibility notes, visual impact, and checklist. It does not link an actual issue and does not use the exact N/A wording required for visual proof, but these are limited omissions in an otherwise complete description.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@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

🧹 Nitpick comments: 1
src/shared/ndjson-retained-chunk-memory.test.ts: 1

55-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The duplicated paused = false; framer.resume() pair is redundant.

Lines 53-54 already perform this sequence. Repeat the call only if a second resume is needed to drain a queued record after a re-pause. In that case, add a comment that says so. Otherwise, remove the duplicate.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5c3a3adf-2915-41ac-9930-0c780b5de9af
📥 Commits

Reviewing files that changed from the base of the PR and between 2db7d5a and 860be75.

📒 Files selected for processing: 19
  • src/main/daemon/daemon-stream-binary-framing.ts
  • src/main/daemon/daemon-stream-binary-retained-memory.test.ts
  • src/main/runtime/mobile-cloud-relay/relay-readiness-retention.test.ts
  • src/main/runtime/mobile-cloud-relay/relay-readiness.ts
  • src/renderer/src/components/editor/combined-diff/CombinedDiffViewer.tsx
  • src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-memory.ts
  • src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-persist.ts
  • src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.test.tsx
  • src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.ts
  • src/renderer/src/components/terminal-pane/pty-preconnect-input-retention.test.ts
  • src/renderer/src/store/slices/removed-worktree-diff-memory.test.ts
  • src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts
  • src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
  • src/shared/abort-signal-reason-memory.test.ts
  • src/shared/abort-signal-reason.test.ts
  • src/shared/abort-signal-reason.ts
  • src/shared/main-process-ndjson-framer.ts
  • src/shared/ndjson-retained-chunk-memory.test.ts
  • src/shared/promise-settlement-waiters.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 53e114e1-bdc6-4214-a17b-6da036c0496f
📥 Commits

Reviewing files that changed from the base of the PR and between 860be75 and 52a5eca.

📒 Files selected for processing: 4
  • src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-cache.ts
  • src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-memory.ts
  • src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts
  • src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

/** A deleted workspace cannot restore its cached file bodies. Ordinary tab closes keep them. */
export function forgetCombinedDiffViewsForWorktrees(worktreeIds: Iterable<string>): void {
const removed = new Set(worktreeIds)
for (const [key, view] of combinedDiffViewStateCache) {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,180p' src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-cache.ts
sed -n '1,240p' src/renderer/src/components/editor/combined-diff/remember-view/combined-diff-view-memory.ts
rg -n 'invalidateCombinedDiffCachesForRelativePath|combinedDiffScroll(Top|Anchor)Cache|forgetCombinedDiffViewsForWorktrees' src/renderer/src/components/editor/combined-diff src/renderer/src/store/slices/worktrees/teardown

Repository: stablyai/orca

Length of output: 8923


🏁 Script executed:

set -o pipefail
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'viewStateKey' -e 'combinedDiffViewStateCache' -e 'combinedDiffScrollTopCache' -e 'combinedDiffScrollAnchorCache' src/renderer/src/components/editor/combined-diff src/renderer/src/store/slices/worktrees/teardown
printf '\n--- restore ---\n'
sed -n '1,230p' src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.ts
printf '\n--- persist ---\n'
sed -n '1,180p' src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-persist.ts
printf '\n--- anchors ---\n'
sed -n '1,190p' src/renderer/src/components/editor/combined-diff/scroll-viewport/use-combined-diff-scroll-anchors.ts
printf '\n--- related cache consumers ---\n'
sed -n '1,180p' src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-memory.ts 2>/dev/null || true
rg -n -F --glob '*.ts' --glob '*.tsx' -- 'getCombinedDiffViewStateKey' 'combined-diff-view-state-key' 'createCombinedDiff' src/renderer/src/components/editor/combined-diff

Repository: stablyai/orca

Length of output: 29780


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- viewer and key callers ---'
sed -n '1,290p' src/renderer/src/components/editor/combined-diff/CombinedDiffViewer.tsx
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'viewStateKey=' -e 'viewStateKey:' -e 'CombinedDiffViewer' src/renderer/src/components/editor/combined-diff src/renderer/src/components/editor | head -160
printf '%s\n' '--- teardown cleanup ---'
sed -n '1,130p' src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts
sed -n '1,130p' src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
printf '%s\n' '--- cache invalidation and direct cache deletion ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'invalidateViewStateCache' -e 'combinedDiffViewStateCache.delete' -e 'combinedDiffScrollTopCache.delete' -e 'combinedDiffScrollAnchorCache.delete' src/renderer/src/components/editor/combined-diff src/renderer/src/store

Repository: stablyai/orca

Length of output: 41517


🏁 Script executed:

set -o pipefail
rg -n -F --glob '*.ts' --glob '*.tsx' \
  -e 'diffViewStateKey' \
  -e 'combinedDiffViewKey' \
  -e 'combinedDiff' \
  src/renderer/src/components/editor/EditorContent.tsx \
  src/renderer/src/components/editor/EditorEditFileSurface.tsx \
  src/renderer/src/components/editor 2>/dev/null | head -220
printf '%s\n' '--- candidate caller context ---'
for f in src/renderer/src/components/editor/EditorContent.tsx src/renderer/src/components/editor/EditorEditFileSurface.tsx; do
  if test -f "$f"; then
    grep -n -C 8 -E 'CombinedDiffViewer|diffViewStateKey|viewStateKey' "$f" || true
  fi
done

Repository: stablyai/orca

Length of output: 23197


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- OpenFile identity and construction ---'
rg -n -F --glob '*.ts' --glob '*.tsx' \
  -e 'type OpenFile' -e 'interface OpenFile' -e 'OpenFile =' -e 'id:.*worktreeId' \
  -e 'worktreeId.*filePath' -e 'filePath.*worktreeId' \
  src/renderer/src/store/slices/editor src/renderer/src | head -240
printf '%s\n' '--- direct scroll-only cleanup outside cache ownership helper ---'
rg -n -F --glob '*.ts' --glob '*.tsx' \
  -e 'combinedDiffScrollTopCache.clear' \
  -e 'combinedDiffScrollAnchorCache.clear' \
  -e 'combinedDiffScrollTopCache.delete' \
  -e 'combinedDiffScrollAnchorCache.delete' \
  -e 'combinedDiffViewStateCache.delete' \
  src/renderer/src | head -160

Repository: stablyai/orca

Length of output: 12138


🏁 Script executed:

set -o pipefail
sed -n '1,115p' src/renderer/src/store/slices/editor/types/open-file.ts
printf '%s\n' '--- combined diff constructors ---'
sed -n '1,245p' src/renderer/src/store/slices/editor/actions/open-combined-diff.ts
printf '%s\n' '--- file id helpers ---'
rg -n -F --glob '*.ts' --glob '*.tsx' -e 'buildOwnedEditorFileId' -e 'buildEditorFileId' -e 'file.id =' -e 'id: crypto' src/renderer/src/store/slices/editor src/renderer/src/shared shared 2>/dev/null | head -180

Repository: stablyai/orca

Length of output: 21005


Clear scroll entries when invalidating a view state.

invalidateCombinedDiffCachesForRelativePath deletes only the view-state entry. Delete the matching scroll-top and scroll-anchor entries in the same branch. Otherwise, a recreated worktree with the same combined-diff key can restore stale scroll state.

🐛 Suggested fix
-import { combinedDiffViewStateCache } from './combined-diff-view-cache'
+import {
+  combinedDiffScrollAnchorCache,
+  combinedDiffScrollTopCache,
+  combinedDiffViewStateCache
+} from './combined-diff-view-cache'
...
     if (cached.sections.some((section) => section.path === relativePath)) {
       combinedDiffViewStateCache.delete(key)
+      combinedDiffScrollTopCache.delete(key)
+      combinedDiffScrollAnchorCache.delete(key)
     }

@OrcaWin OrcaWin changed the title fix(memory): release consumed buffers, abandoned waits, and deleted diff views [mem] fix(memory): release consumed buffers, abandoned waits, and deleted diff views Oct 19, 2026
…t re-create cached diff entries

Combined-diff view state, scroll offset and scroll anchor now live in one owner-tagged
entry per view key. A mounted viewer claims its key; writes only update claimed entries,
so the late unmount write that follows a workspace removal is a no-op instead of
re-adding orphan scroll entries.
- Combined diff: writes go through a per-mount lease. Forgetting a workspace revokes its
  leases, so a late unmount write cannot resurrect entries, while an LRU-evicted entry of
  a still-mounted viewer is re-created on its next write. Drops the duplicated view scrollTop.
- NDJSON framer: copy a retained tail only when it is a strict slice of its chunk, copy an
  unfinished line once, and copy every rejected-line prefix.
- Binary frame reader: single exit path for the end-of-feed leftover copy.
- Relay readiness reuses waitForPromiseWithSignal instead of re-implementing it.
- PTY preconnect input: pass the listener-removing settle into createInput instead of
  mutating the input afterwards.
@nwparker
nwparker merged commit ad5ef0d into main Oct 19, 2026
60 of 63 checks passed
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.

2 participants