You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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: add maxBytes (e.g. 16–32MB) alongside maxLines; evict oldest chunks when EITHER limit is exceeded. The repaint nudge makes front-truncation safe.
Cap before concat: walk chunks from the tail collecting only up to REPLAY_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.
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).
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 whenlineCount > maxLines(10,000), counting\nbytes. A full-screen TUI (Claude Code) emits almost no newlines — its output is cursor-movement redraws — solineCountbarely 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.tsWELCOME 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_MAX8MB) is applied AFTER the concat viasubarray, 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.pushDatakeeps the newline-lesspartial"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, andgetAll()/getSince()copy it per attach.Fix directions
ShellperReplayBuffer: addmaxBytes(e.g. 16–32MB) alongsidemaxLines; evict oldest chunks when EITHER limit is exceeded. The repaint nudge makes front-truncation safe.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.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.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).