Skip to content

fix(cmux-tui): the reap grace holds at most 16 closed terminals; past it the longest-closed ends at once (STATUS 7998) - #33117

Merged
lawrencecchen merged 3 commits into
feat-cmux-nextfrom
pty-grace-cap
Nov 3, 2026
Merged

lawrencecchen merged 3 commits into
feat-cmux-nextfrom
pty-grace-cap

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Refs STATUS 7998 (VIDEO EXTRA: 50 tabs closed at once kept all 208 /dev/ptmx handles 10 s later and released them by 60 s; at the L6 storm's close rate, closed-but-held PTYs reach the machine's 511 in about 48 s).

Cause

What keeps a closed terminal's host process and its PTYs alive 10-60 s after the close is the reap grace (mux/terminal_reap.rs; the app starts the daemon with --terminal-reap-grace-seconds 30, DaemonLauncher.terminalReapGraceSeconds). A closed tab's terminal keeps its shell for the grace so the close can be undone with the live shell. A shell cannot outlive its PTY, so "keep the process but release its PTY" is not possible: the PTY goes when the shell ends. Measured on the fleet (#32838's diag step b01dd8303d952fd412d5199f): each terminal holds 4 handles (3 in its host, 1 daemon custody copy), all for one PTY, and all come back when the grace ends. #32838 (merged) ends those terminals only when free PTYs fall under 64, which a storm passes faster than a reclaim pass.

Fix

The reap grace holds at most MAX_TERMINALS_IN_REAP_GRACE (16) closed terminals. On each reaper pass (a topology change wakes it), the closed terminals past the cap, longest closed first, are reaped at once through the reaper's own guarded close (screen archived as at any reap; never a kept terminal or one a client is attached to). A recent close stays undoable with its live shell. At the L6 storm's rate (about 5 closes a second) the closed-but-held PTYs stay at 16 instead of about 150.

Tests

mux::terminal_reap::tests::closed_terminals_past_the_grace_cap_end_at_once (18 workspaces, closed in a burst inside the grace, reaper after each close): the 2 longest-closed end, the 16 recent stay running, a placed terminal stays.

Observed / Not run

Observed: the red step and the per-process PTY counts in #32838.

Not run: the L6 storm journey again (needs VIDEO EXTRA's kit); a GUI run.

Decision

The cap value, 16: lead decision (a script burst is the case that hit the PTY limit; 16 undoable live closes is enough for a person). A close past it is reopened from its saved screen with a new shell.

Changelog

  • Fixed: cmux-next no longer runs out of terminals ("PTY capacity exhausted") when a script or agent opens and closes many tabs in a minute: at most 16 recently closed terminals keep their shell.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes PTY exhaustion when a script or agent closes many tabs in a burst: the reap grace now holds at most 16 closed terminals instead of letting every closed tab keep its shell (and so its PTY) for the full grace. Past the cap, the longest-closed terminals are reaped at once, with the screen archived as at any reap; a recent close stays undoable with its live shell.

  • At the old behavior, about 5 closes per second held ~150 extra PTYs and hit the machine's 511-terminal limit in about 48 seconds.
  • Adds a test that closes 18 terminals in a burst and verifies the 2 longest-closed end while the 16 recent stay running.

Written for commit 99a11c6. Summary will update on new commits.

View guided diff Turn on auto-fix

lawrencecchen and others added 3 commits November 3, 2026 04:23
… it the longest-closed ends at once (STATUS 7998)

Each closed tab's terminal waited out the whole reap grace (30 s in the
app) with its shell, and a shell cannot outlive its PTY (4 /dev/ptmx
handles a terminal: 3 in its host, 1 daemon custody copy). A script
closing about 5 tabs a second (the L6 storm) held about 150 PTYs at once
on top of the live ones and reached the machine's 511 in 48 s, so every
new tab was refused. The reaper now ends, on the same pass that sees
them, the longest-closed terminals past MAX_TERMINALS_IN_REAP_GRACE (16):
a recent close stays undoable with its live shell; an older one in a
burst is reaped as at the end of its grace (screen archived, never a
kept or attached terminal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oses and reaps (STATUS 7998)

A create after a reap trips the scoped-projection cross-check on the
base too (diag step 0ff771ada37a: a normal reap at the grace end, then
new_workspace, panics the same way); that is a separate defect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 99a11c6: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr33117 -f ref=pty-grace-cap -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 1382711 to compare; do not merge until green; see lane-rules

Shipped in:

@blacksmith-sh

blacksmith-sh Bot commented Nov 3, 2026

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpb3vrjtlx/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpers3eu67/summary-fail.json (live):/summary-fail.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (subagent, correctness first): SHIP at 99a11c6

I read the code only. I did not build or run tests.

1. Which terminals the cap can end. reapable is the catalog terminals that have a host identity, no live surface of their runtime, no durable placement, and no keep (mux/terminal_reap.rs:218-263). The cap cannot end a terminal that is unplaced only for a moment:

  • Adoption: a terminal that a restarted owner is still adopting is not in the catalog yet, so it is not reapable, and its row keeps its time. A restored terminal gets its durable placements before adoption (terminal_runtime_index.rs:64-98).
  • Moves and undo: tab moves and drags between workspaces and layout undo are single commit plans (resource_topology/tab_workspace_move.rs). The reaper reads the catalog and the placements under one state lock. The guarded close checks the placement and keep again under the registry lock (terminal_close.rs:126-139), so a terminal that is placed again is Retained.
  • New tabs: a new tab's terminal goes into state.surfaces together with its catalog entry (detached_terminals.rs insert_created_terminal), so it counts as placed.
  • HiddenTabClose: it is app-only and sends nothing, so the daemon's placements stay until the close commits.
  • Remote and detached terminals: remote/detached terminals are kept from the commit that reserves them. Remote moves keep the terminal before they close a tab.
  • Disconnect: a client disconnect removes only views, not placements.

2. Attached terminals. No busy loop. An Attached result emits no event and is postponed by at least MIN_RETRY, and the reaper wakes only on events or deadlines (terminal_reap.rs:408-416).

3. Bookkeeping. Every terminal past the cap is in reapable, so observe_unplaced gave it a deadline first, and postpone/forget work on it. run_all returns results in input order (terminal_work.rs:114-136), and the zip with due is correct. Dedup against due is correct.

4. Ordering. The sort is b.0.cmp(a.0): the longest unplaced goes first, and ties go to the lower terminal id. checked_sub(...).filter(>0) gives no terminals at 16 and one at 17.

5. Test. The test is meaningful. In the loop, the pass at the 17th and 18th close ends t0 and t1. Without the cap logic nothing is reaped (left: []). The test does not cover two or more cap reaps in one pass (the run_all path).

Non-blocking notes:

  • Attached or failing unplaced terminals count toward the 16 (terminal_reap.rs:151-160). When they are the oldest, they use the slots past the cap, so more PTYs stay held (fail-safe). A reap that keeps failing now reports a diagnostic on every topology-event pass, not once per grace.
  • The unplaced time is the reaper pass that first saw the terminal. One batch close therefore gives equal times, and the terminals are ended in id order, not close order.
  • The cap applies even when a user sets a longer grace (up to one week). For closes past 16, Reopen Closed Tab and the undo toast fall back to the archive: a new shell with the archived screen, and the running program is lost. This is in the cap value decision.
  • The green test steps at this head are not read yet (controller outage, PR body). Observe them before landing. The only red CI check, "No fleet host names in gallery fixtures", is not related to this Rust-only change.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 3, 2026
@lawrencecchen
lawrencecchen merged commit 4623979 into feat-cmux-next Nov 3, 2026
237 of 254 checks passed
@lawrencecchen
lawrencecchen deleted the pty-grace-cap branch November 3, 2026 06:51

This branch was previously deployed

3 inactive deployments
artifacts — 99a11c62 Deployed Nov 3, 2026 by lawrencecchen via publish tree-addressed cmux-tui #104061
sentry-dsyms — 99a11c62 Deployed Nov 3, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #104061
ci-routing-agent — 99a11c62 Deployed Nov 3, 2026 by lawrencecchen via cmux-tui tree preflight #104061
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant