Repository navigation
Shellper replay buffer is unbounded for no-newline TUI streams; full Buffer.concat on every connect → multi-GB allocation spikes when opening a session #1253
Description
Symptom (reported 2026-07-26)
Opening an existing terminal session sometimes allocates several GB (observed up to ~10GB), causing memory pressure and sometimes process termination.
Root cause — two compounding defects, both confirmed in source
1. Line-count-only eviction never fires for TUI streams (shellper-replay-buffer.ts)
ShellperReplayBuffer.append() evicts oldest chunks only when lineCount > maxLines (10,000), counting \n bytes. A full-screen TUI (Claude Code) emits almost no newlines — its output is cursor-movement redraws — so lineCount barely advances and eviction never triggers. chunks[] grows unboundedly for the life of the shellper (which survives Tower restarts by design). Builders are the worst case: weeks of continuous test/tool output. Trajectory already evidenced in the #1198 incident (17.6MB/17.7MB replay buffers after ~a week of light architect use); heavy builder sessions reach GBs.
2. getReplayData() full-concats on EVERY connect (shellper-process.ts WELCOME path)
Buffer.concat(this.chunks) materializes the entire accumulated history into one new Buffer per client connect — transiently doubling the shellper's footprint at exactly the moment a user opens the session. A ~5GB buffer → ~10GB peak → the reported spike. The #1198 send cap (REPLAY_PAYLOAD_MAX 8MB) is applied AFTER the concat via subarray, so it bounds the wire, not the allocation. On pre-#1204/#1218 binaries (i.e., every installed deployment today) there is no send cap either, so the full payload additionally ships and is buffered again Tower-side.
Secondary same-class defect (Tower-side, slower)
RingBuffer.pushData keeps the newline-less partial "whole and unbounded" (ring-buffer.ts:37-41) with the documented rationale that truncating would corrupt alt-screen replay. That rationale is obsolete: since #1198, viewers repaint via the post-connect resize nudge, so front-truncation no longer breaks correctness. Live evidence on the originating machine: the #1047 partial monitor shows max partial at ~2.6MB growing ~1MB/hour on the busiest session — months-scale GB risk, and getAll()/getSince() copy it per attach.
Fix directions
- Byte-cap eviction in
ShellperReplayBuffer: addmaxBytes(e.g. 16–32MB) alongsidemaxLines; evict oldest chunks when EITHER limit is exceeded. The repaint nudge makes front-truncation safe. - Cap before concat: walk
chunksfrom the tail collecting only up toREPLAY_PAYLOAD_MAX, and concat just those — O(cap) allocation per connect instead of O(history). With fix 1 in place this becomes belt-and-suspenders, but it's the cheap guard that directly kills the open-time spike even for legacy-accumulated buffers. - Cap the Tower
RingBufferpartial (e.g. keep the last N MB) now that the nudge covers repaint; update the tower + vscode: terminal freeze from oversized-replay reconnect storm (unbounded no-newline buffers + client backpressure loop) #1047 monitor from observe-only to observe-and-enforce. - Observability: shellper logs its buffer size at WELCOME (new binaries already log when the send cap trims);
afx status/Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227's fleet RSS view will surface a ballooning shellper once deployed.
Deployment interaction
Every shipped fix in this area (#1204's frame tolerance, #1218's send cap) is merged-but-uninstalled on affected machines; installed binaries are the worst case (unbounded accumulate + uncapped send + fatal oversized-frame handling). Fix 1/2 land in the shellper binary, which — per the detached-shellper design — only takes effect for shellpers spawned AFTER the upgrade; long-lived pre-upgrade shellpers keep their accumulated buffers until restarted or reaped. Worth a release-notes remediation note (restart heavy long-lived sessions after upgrading).
Related
#1047 (line-based wire contract + partial observability), #1198/#1204 (oversized-REPLAY incident: the 16MB frames were this bug's earlier, smaller manifestation), #1218 (send cap — wire-level only), #1227 (fleet memory pressure; this is the per-session sibling of that fleet-level issue).
Activity
amrmelsayed commentedon Jul 25, 2026
Duplicate of #1205 (pre-existing, filed 2026-07-19 with the same root cause and a stronger structural fix direction — O(screen) replay via headless terminal-state emulation). This issue's new findings (the open-time Buffer.concat allocation spike explaining the multi-GB-on-open symptom, the 10GB field escalation, interim cap-before-concat/byte-cap-eviction directions, Tower-partial sibling, deployment note) are folded into #1205 at #1205 (comment) — that issue is canonical.