Skip to content

bug: cols-gate purge storms wipe claude-code scrollback — resize churn, not fork width, is the launch-resume corruption #373

Description

@yicheng47

Description

The v0.4.7 [launch-dims] instrumentation caught the production launch-resume corruption in the act, and it is not a width-estimate bug. The resume forks were correct — resume-on-launch fork: … size=199x59 source=caller-supplied — and the whole 0038 dims pipeline worked as designed. What destroys the terminal afterwards is a storm of cols-gate ring purges: for claude-code every cols change purges the entire output ring (runtime_clears_on_resize, no coalescing), and post-launch geometry churn produces dozens of cols changes in seconds. Each purge drops everything — including the resume transcript claude just repainted — and after the storm claude's SIGWINCH repaint restores only its recent frame. Net effect: a resumed claude-code pane with no scrollback and, mid-storm, visibly wrong rendering.

From ~/Library/Logs/com.wycstudios.runner/runner.log, one restart at 08:59–09:00 (2026-07-29), ~50 purges in 90 seconds across three claude-code sessions:

08:59:34 resume-on-launch fork: session=01KXZ1X4… runtime=claude-code size=199x59 source=caller-supplied
08:59:43 [launch-dims] first-fit session=01KYP8HE2J… 165x57
08:59:45 [launch-dims] first-fit session=01KXHY1Q… 78x23
08:59:45 cols-gate purge: session=01KXHY1Q… cols 205 -> 78
08:59:45 cols-gate purge: session=01KXHY1Q… cols 78 -> 76
08:59:45 cols-gate purge: session=01KXHY1Q… cols 76 -> 209
08:59:45 cols-gate purge: session=01KXHY1Q… cols 209 -> 76
08:59:47 cols-gate purge: session=01KXHY1Q… cols 76 -> 210 … 217 … 233 … 240 … 243
08:59:47 cols-gate purge: session=01KYP8HE2J… cols 166 -> 173 … 188 … 195 … 197
08:59:48 cols-gate purge: session=01KYP8HE2J… cols 196 -> 191 … 178 … 170 … 166 … 165
09:00:29 cols-gate purge: session=01KYP8HE2K… cols 174 -> 176 … 194 … 205 … 208 -> 209
09:00:30 cols-gate purge: session=01KYP8HE2K… cols 209 -> 208 … 196 … 181 … 175 -> 174

Three distinct defects are visible:

  1. No coalescing on the purge. The smooth up-then-down sweeps (174→…→209→…→174) are live-drag geometry; every intermediate width purges the full ring and forces a claude repaint. A single sidebar/window/gutter drag obliterates scrollback dozens of times, and the final width often equals the starting width — the entire storm was for nothing.
  2. A transient garbage fit gets pushed. Session 01KXHY1Q… (steady-state ~209 cols) got a first-fit of 78×23 from a not-yet-settled container, and that measurement was pushed to the PTY — purging the ring (205 → 78) and making claude repaint mis-wrapped at 78 cols before geometry corrected.
  3. Two writers fight over one session. The 76 → 209 → 76 → 210 oscillation is two surfaces alternately pushing different sizes for the same session, each flip purging again — the bug: two windows on the same chat — terminal size state desyncs across primary flips (stale lastPushed dedupe, dual-primary push race, wrong-cols replay) #313 desync class appearing in a single-window launch.

Why this always read as "production-only": it is layout-churn-dependent, not build-dependent. The dev profile's smaller tab/split/window state produces one clean fit and at most one purge at launch; the production profile's restored multi-tab layout produces transitional containers and churn. Five prior fixes (0027, 0035, 0037, 0038, #365) targeted the fork width, which this log shows was already correct.

Supersedes the mechanism theory in #366 (its estimate-mixing claim was already disproven in the comment thread; the estimate path is now verified working by this log). Related: #313 (defect 3 is the same class), #306 / impl 0020 (why the purge exists at all).

Expected behavior

  • A continuous drag (window edge, sidebar, chat panel, split gutter) results in one purge and one repaint at the final width — not one per intermediate width.
  • A measurement from an unsettled or transitional container is never pushed to the PTY.
  • Exactly one surface — the visible owning pane — pushes sizes for a session; a background/hidden mount never fights it.
  • After a launch with auto-resume, a claude-code pane retains the transcript its resume repaint produced.

Steps to reproduce

  1. Production profile with several chat tabs (including splits) and running claude-code sessions; quit and relaunch with auto-resume on.
  2. Watch ~/Library/Logs/com.wycstudios.runner/runner.log: cols-gate purge storms fire during launch settling without any user input.
  3. Or, at steady state: drag the sidebar or a split gutter across a claude-code pane — one purge per intermediate width; scrollback gone at the end of the drag.

Relevant code

  • src-tauri/src/session/manager/output.rs:538-546 — the cols-gate: compares last_pty_cols, purges immediately per change; no quiescence.
  • src-tauri/src/session/manager/output.rs:796runtime_clears_on_resize (claude-code, codex, qoder, trae all affected; claude-code hurts most because its repaint restores only the recent frame).
  • src/components/RunnerTerminal.tsxpushSize / ResizeObserver path that forwards every fit, including the transient 78×23 one; first-fit logging added in fix(mission): size backend-started forks and log launch widths #372 marks the push site.
  • src/components/ChatPaneGroup.tsx — hidden-pool wrappers whose geometry sync is the likely source of transitional boxes and the second writer.
  • docs/impls/archive/0024-resume-scrollback-preservation.md decision 5 — accepted resize-purge behavior on the assumption resizes are rare and user-driven; the log falsifies that assumption at launch.

Suggested fix

Fix the churn, not the purge semantics — given genuinely stale-width bytes, purging remains correct (impl 0020 / #306).

  1. Trailing-debounce the PTY resize push (~150–200ms quiescence, frontend or backend): a drag collapses to one ioctl, one purge, one repaint. Skip the purge entirely when the settled cols equal the ring's current cols (round-trip drags become free).
  2. Settle-gate the first fit: do not push a measurement taken while the pane's container is transitional (mid-hydration, hidden-pool geometry not yet synced); the existing windowSettle/layout-hydration signals mark when a box is trustworthy.
  3. Single-writer per session: only the visible owning pane pushes sizes; background mounts observe without writing — kills the 76↔209 oscillation and overlaps with bug: two windows on the same chat — terminal size state desyncs across primary flips (stale lastPushed dedupe, dual-primary push race, wrong-cols replay) #313's fix.

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Real friction — fix this cyclebugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions