Skip to content

Release the PTY renderer-delivery queue on every teardown path; guard the synthetic-kill window (STA-1397)#7972

Open
brennanb2025 wants to merge 3 commits into
mainfrom
brennanb2025/sta-1397-main-mem-creep
Open

Release the PTY renderer-delivery queue on every teardown path; guard the synthetic-kill window (STA-1397)#7972
brennanb2025 wants to merge 3 commits into
mainfrom
brennanb2025/sta-1397-main-mem-creep

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Completes the STA-1397 main-process memory-creep fix on top of #7630 (merged), which independently landed the same 2 MB pendingData cap + droppedBacklog snapshot-restore flag this branch originally carried. After rebasing onto that work, this PR keeps the two failure modes #7630 does not cover — both proven by heap-snapshot forensics on the profiling rig (details on Linear STA-1397):

  1. Stranded delivery state on teardown. pendingData and rendererInFlightCharsByPty are closure-local to registerPtyHandlers; only sendPtyExitToRenderer released them. Teardown paths that skip it — SSH connection shutdown (clearPtyOwnershipForConnection) and the synthetic-kill dedup early-return — stranded up to a full cap's worth of queue per PTY for the process lifetime, and, worse, left the PTY's in-flight chars counted against the global 8 MB ACK budget forever, silently throttling delivery for all terminals until app restart. In the rig repro, 3 of 17 terminals stranded ~36 MB each through a plain worktree removal (pre-cap; still 2 MB + wedged budget each post-#7630). Fix: a clearPtyRendererDeliveryState hook that clearProviderPtyState now calls on every teardown path — it flushes the queued tail first (preserving the "last ≤8 ms of output" guarantee regardless of call order relative to sendPtyExitToRenderer), then releases the queue and the in-flight budget.

  2. Repopulation inside the synthetic-kill window. pty:kill sends a synthetic exit that flushes and deletes the entry, but onData had no kill-window guard: the daemon's socket backlog keeps draining output for seconds after the kill, repopulating pendingData, and the real exit is then swallowed by consumeSyntheticKillExit — so nothing ever cleans the entry again. Fix: drop renderer delivery for ids inside the dedup window (runtime/status consumers above still see every byte).

Also extends the #7630 platform analysis: the never-ACK wedge is not Windows/Linux-only. A window that has never mounted a terminal pane never attaches the renderer PTY dispatcher at all (ensurePtyDispatcher is transport-created), so nothing ACKs regardless of background-throttling — reproduced on macOS with CLI-created terminals in background worktrees (141 → 568 MB heapUsed in 3 min, zero recession).

Diff after reconciliation

src/main/ipc/pty.ts + src/main/ipc/pty.test.ts only. The cap, flag, preload/dispatcher/renderer threading are #7630's as merged; this branch's duplicate implementation and duplicate cap test were dropped in the merge commit.

Screenshots

No visual change.

Testing

  • pnpm lint (oxlint clean on changed files)
  • pnpm typecheck (node + web)
  • pnpm test — full src/main/ipc/pty.test.ts suite (227 tests incl. #7630's cap tests) + 419 renderer terminal-pane tests
  • 2 regression tests added: provider-state teardown flushes the tail and zeroes queue + in-flight budget; synthetic-kill-window output cannot repopulate the queue
  • Rig-validated end-to-end before the rebase (17 hidden terminals, ~1 MB/s each, 3 min): burn heapUsed floor ~90 MB vs 250–400 MB rising; post-close heapUsed returns to the 49 MB idle baseline vs 160 MB with 108 MB stranded

AI Review Report

Checked the merge reconciliation specifically: no remnants of the superseded dropped flag or 1 MB cap constants remain (grep-verified); the teardown hook uses #7630's makePtyDataPayload signature and passes droppedBacklog through the exit flush; hook is assigned before any teardown can run (module-level no-op default otherwise); flush-then-clear ordering analyzed against both clearProviderPtyState-before-sendPtyExitToRenderer (daemon/SSH exits) and the reverse (kill paths) — no double-send, no tail loss. Cross-platform: pure IPC/queue bookkeeping, no platform branches; original evidence spans Windows (#7375 profiling) and macOS (rig).

Security Audit

No new input handling, command execution, path handling, auth, or secrets. Changes are cleanup/bookkeeping on existing pty:data delivery structures; the kill-window guard only drops renderer-bound delivery for a PTY already being torn down.

Notes

  • Follow-ups tracked on Linear STA-1397: daemon-side unbounded socket write buffering to a slow main (1.9 GB RSS during the rig burn) deserves its own ticket; the bounded main-side headless emulators (~8 MB/terminal) could take an idle-eviction cap.
  • The merge commit intentionally adopts #7630's naming/mechanism wholesale to avoid two flags meaning the same thing; a small follow-up commit restores main's exact formatting on 7 unrelated files that the local pre-commit hook had rewrapped during the merge.

…own path (STA-1397)

Root cause of the monotonic main-process memory creep under sustained
multi-terminal load: pendingData (the main->renderer PTY delivery batcher in
ipc/pty.ts) accumulates every output chunk via string append, and its drain is
gated on renderer pty:ackData with 512KB/PTY + 8MB global in-flight caps. Panes
that never ACK — hidden worktrees, windows that never mounted a terminal pane —
wedge the window shut, so main retained the entire output of every hidden
terminal as cons-string trees (~312MB after a 3-minute 17-terminal burn; heap
snapshots + live-retained allocation profile in the Linear ticket). Worse, the
map is closure-local and only sendPtyExitToRenderer deleted entries, so SSH
connection shutdown and the synthetic-kill dedup window stranded entries (and
their in-flight budget, throttling ALL terminals) for the process lifetime.

Fix:
- Cap pendingData per PTY (1MB, trim to the newest 640KB). Seq stays truthful
  for the kept tail; the next payload carries dropped:true and the renderer
  routes it into the existing hidden-output snapshot-restore machinery instead
  of painting the spliced stream.
- clearPtyRendererDeliveryState hook: clearProviderPtyState now flushes and
  releases the queue + in-flight budget on every teardown path (SSH connection
  drop, synthetic kill, daemon spawn failure), not just renderer-observed exits.
- Drop renderer delivery for output arriving inside the synthetic-kill dedup
  window; the daemon's socket backlog can drain for seconds after pty:kill and
  used to repopulate the just-deleted entry with no cleanup left to run.

Validated on the profiling rig (17 hidden terminals, ~1MB/s each, 3min):
heapUsed churn floor 90MB vs 250-400MB rising before; post-burn+GC retained
strings 55MB vs 348MB; pendingData entries bounded ~1.3MB each vs 14-23MB
unbounded; no entries survive worktree removal.

Co-authored-by: Orca <help@stably.ai>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

PTY provider cleanup now flushes remaining renderer data and releases pending and in-flight delivery bookkeeping. PTY output received during the synthetic-kill deduplication window is ignored. Tests cover both teardown under backpressure and late output after a synthetic kill.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main PTY teardown and synthetic-kill guard changes.
Description check ✅ Passed The description includes all required template sections and provides concrete testing, review, security, and notes details.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

brennanb2025 and others added 2 commits July 9, 2026 14:12
#7630 landed the same 2MB cap + droppedBacklog restore flag this branch
introduced independently — adopt its mechanism wholesale (constants, flag
naming, renderer threading, cap tests). This branch keeps what #7630 does not
cover: releasing the queue + in-flight ACK budget on teardown paths that skip
sendPtyExitToRenderer (SSH connection drop, synthetic-kill dedup), and dropping
renderer delivery for output arriving inside the synthetic-kill window.

Co-authored-by: Orca <help@stably.ai>
…t hook during the merge

Co-authored-by: Orca <help@stably.ai>
@brennanb2025 brennanb2025 changed the title Bound main→renderer PTY delivery queue and release it on every teardown path (STA-1397) Release the PTY renderer-delivery queue on every teardown path; guard the synthetic-kill window (STA-1397) Jul 9, 2026
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