Skip to content

fix(session): coalesce resize storms, gate size pushes to the owning pane - #374

Merged
yicheng47 merged 2 commits into
mainfrom
fix/373-resize-churn-purge-storms
Jul 29, 2026
Merged

fix(session): coalesce resize storms, gate size pushes to the owning pane#374
yicheng47 merged 2 commits into
mainfrom
fix/373-resize-churn-purge-storms

Conversation

@yicheng47

Copy link
Copy Markdown
Owner

Fixes #373.

The v0.4.7 [launch-dims] instrumentation proved the launch-resume corruption was not a fork-width bug: forks landed at caller-supplied real widths, and what destroyed the panes was ~50 cols-gate ring purges in 90 seconds of post-launch layout churn — each purge dropping the resume transcript claude had just repainted. Purge semantics themselves stay (impl 0020 / #306): given genuinely stale-width bytes, purging remains correct. This PR removes the churn that fed it.

The three defects → three fixes

  1. No coalescing → backend trailing debounce with round-trip skip. Cols changes for clears-on-resize runtimes park in SessionState::pending_resize; every further push folds in and extends the deadline; a per-storm settle thread applies one final ioctl after 175ms quiescence and purges only when the settled cols differ from the cols the ring holds. A drag that ends where it began costs zero purges and exactly one restoring repaint (rows-nudge, since a same-size TIOCSWINSZ is a kernel no-op and the owner pane cleared its viewport during the drag). Rows-only resizes stay synchronous — preserving the activation dance's SIGWINCH nudge — as do shells. update_last_size stays prompt, and a failed ioctl still cannot purge. The settle runs as one critical section under the session state lock, aborting on killed/resuming/handle-gone: kill sets killed under that lock before runtime.stop begins and resume sets resuming before runtime.spawn can overwrite the id→PTY mapping, so a stale settle can never physically resize a replacement child.
  2. Transient garbage fit (78×23 against a ~209-col session) → placement settle-gate. createPaneGeometry (moved to src/lib/paneGeometry.ts) holds unplaced wrappers at 0×0 until they're positioned from a real pane-body rect — an unplaced absolute wrapper otherwise shrink-wraps xterm's default 80×24 canvas into a measurable, meaningless box. Zero size keeps the pane unmeasurable, so the existing rect guards hold every fit and push.
  3. Two writers oscillating (76↔209↔76↔210) → single-writer per session. sizePushVerdict gates every backend size push: only the visible owning pane writes; hidden-pool wrappers and background tabs refit locally without writing; activation hands ownership over. Single-window case only — 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 multi-window protocol is out of scope.

Instrumentation extends #372 in the same runner.log: cols-gate purge … (N coalesced), cols-gate settle … round-trip, purge skipped, repaint nudged, and frontend [resize-gate] suppression/placement lines. Docs: correction note on impl 0024 decision 5, full decision record in docs/impls/0039-resize-storm-coalescing.md.

Tests

  • Rust: storm collapses to one settled ioctl/purge; round-trip storm → zero purges + nudge pair; failed settled ioctl purges nothing and leaves the gate unmoved; rows-only stays synchronous and keeps the ring; settle thread fires unaided; settle inside kill's blocking-stop window aborts untouched; stale settle after kill+respawn touches neither the fresh PTY nor its ring.
  • Frontend: sizePushVerdict (owner pushes, transitional/non-owner push nothing), shouldClearViewportBeforePush (clears pair only with real pushes), createPaneGeometry placement gate.

Checks

cargo test --workspace 562 passed / 0 failed · pnpm exec tsc --noEmit clean · pnpm run lint 0 problems · pnpm test 258 passed. Peer-reviewed via Runner crew: two must-fix rounds (round-trip blank viewport, settle/lifecycle race) resolved; final review clean.

Manual smoke (post-merge)

  • Drag sidebar / split gutter / window edge across a live claude-code pane: scrollback survives, at most one repaint at drag end; a round-trip drag keeps its full ring.
  • Quit + relaunch with auto-resume: log shows pane-placed before first-fit, coalesced settle lines, no transient-width purges; scroll history intact.

🤖 Generated with Claude Code

…pane (#373)

Production logs showed ~50 cols-gate ring purges in 90s of launch layout
churn, wiping the resume transcripts the correctly-sized forks had just
painted. Three fixes, purge semantics unchanged:

- Trailing-debounce cols changes for clears-on-resize runtimes in
  SessionManager::resize (175ms quiescence): one settled ioctl per
  storm, purge only when the settled cols differ from the ring's; a
  round-trip storm skips the purge and forces the restoring repaint via
  the rows nudge. The settle runs as one critical section under the
  state lock, aborting on killed/resuming/handle-gone, so it can never
  target a child spawned after the storm. Rows-only and shell resizes
  stay synchronous; update_last_size stays prompt; a failed ioctl still
  cannot purge.
- Settle-gate transitional panes: unplaced terminal wrappers are held at
  0x0 until pane geometry sync places them from a real rect, so a
  shrink-wrapped 80x24 canvas can no longer be measured and pushed as a
  ~78x23 garbage fit.
- Single-writer per session: sizePushVerdict routes every backend size
  push; only the visible owning pane writes, hidden-pool and background
  mounts refit locally without writing, and the owner-only viewport
  clear pairs 1:1 with pushes the settle will repaint.

Extends the #372 instrumentation in runner.log (coalesced counts,
round-trip settles, suppressed pushes, pane placement), corrects the
resize-frequency assumption in impl 0024 decision 5, and records the
decisions in docs/impls/0039-resize-storm-coalescing.md.
@yicheng47
yicheng47 merged commit 6fb2d0c into main Jul 29, 2026
2 checks passed
@yicheng47
yicheng47 deleted the fix/373-resize-churn-purge-storms branch July 29, 2026 11:55
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.

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

1 participant