fix(shutdown): flat post-exit pause and a budget sized from measurement - #117
Merged
Conversation
Two problems, both found by running it rather than reasoning about it. 1. The adaptive config gate overshoots at shutdown too. #111 reverted it on the launch path but KEPT it here, on my reasoning that "the machine is quiet at shutdown, so polling is reliable". Measurement falsified that. A real run logged: SHUTDOWN 'SiteAnalysis': exit=4656ms cfgSettle=8731ms against a 1000ms cap — 8.7x over, and 56% of the entire shutdown budget spent in one session, which is what forced the remaining six to be killed with no exit wait. Same disease as the launch path: when the thread stalls, the gate measures the stall rather than the file. Now a flat Task.Delay, on both the shutdown loop and the restart path. Recomputing that run flat gives 8969ms instead of 15506ms with nothing force-disposed — the gate's typical ~300ms beats a flat 1000ms right up until it doesn't, and the tail is what costs. ClaudeConfigGate now has no callers and is deleted along with its tests, rather than left as dead code for someone to reintroduce. 2. The 15s budget was sized from the wrong data. It was chosen when the only measurements available were idle sessions exiting in 460-770ms. Busy sessions measure 2.3-4.7s each, so nine of them need roughly 30s, and 15s force-disposed over half the fleet on an ordinary close. Raised to 30s: a clean exit lets Claude finish writing its config, and ShutdownOverlay is already on screen explaining the wait. The budget is there to bound a wedged session, not to hurry a healthy one. Confirmed while here: the HasExited fix from #115 works. No exit= value is near the 10s cap (max observed 4656ms), so nothing is timing out on a process that already died — which was the whole point. CLAUDE.md updated: the gate is gone from both paths, and the budget carries its measurements plus a note to re-measure exit= before shrinking it, since the summary line cannot distinguish slow exits from waits that never return. 304/304 pass (10 fewer — ClaudeConfigGateTests removed), 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be
AThraen
added a commit
that referenced
this pull request
Sep 6, 2026
…ally visible Two waiting states, treated differently on purpose. STARTUP — restore rail A 25-session restore runs ~131s with per-session launch cost swinging 12x (0.6s to 10.9s, measured from crash.log). There is no steady rate to extrapolate from, so an indeterminate spinner reads identically at session 2 and session 22. RestoreRail (2px, under the toolbar) plus a "7 / 25 restoring" pill give the aggregate k-of-N; per-session state stays on the existing placeholder sidebar rows. Restore does NOT block the user — placeholder rows mean you can click into a session the moment it lands — so this stays deliberately out of the way. Nothing modal. The counter advances after the try/catch, not inside it, so a session that fails to restore still moves the rail. Otherwise one bad session strands it short of full, which reads as exactly the hang it is meant to rule out. SHUTDOWN — the overlay was invisible, and that is the real bug Reported as "more like 1 line - thats hard to see. no spinner or anything". The markup looked correct: a Grid sibling with Panel.ZIndex="100" and a Storyboard-rotated arc dead centre. WebView2 is an HwndHost. A native child window is composited by the OS on top of everything WPF renders, and Panel.ZIndex does not enter into it. The overlay was being drawn BEHIND every terminal pane. What reached the user was scrim leaking through the few-pixel gaps between panes — hence a stray line and no spinner, because the spinner sat centred over a pane. The codebase already documents HwndHost for input (#108, three separate call sites). Nobody had written down the output half, which is the more expensive one to rediscover because the code looks right. Fix: collapse TerminalGrid before showing the overlay. Safe — every pane is about to be disposed, and a frozen terminal is worth nothing mid-shutdown. With it visible, it earns being more than a spinner. The board names every session and ticks it off (· pending, ◐ closing, ✓ clean, ⨯ force-disposed), with elapsed time per session, a k/N bar, and a budget bar against ClaudeShutdownBudgetMs. Force-disposed sessions are marked rather than hidden: that is the case a user most wants to see and it used to happen completely silently. ShutdownHint escalates with elapsed time to explain why the wait is long. Explanatory, not jokey — it has to read well on the four-hundredth shutdown, which is the argument against the "be patient" copy I originally sketched. Also here: - Board list scrollbar restyled (scoped to that ScrollViewer). Any setup past ~14 sessions scrolls, so the stock system-coloured bar was on screen for most shutdowns, not an edge case. - Board skipped entirely at zero sessions, so --clean runs don't flash a full-window "0 / 0". - Corrected a stale comment claiming the adaptive config gate is "still used at SHUTDOWN". #117 removed it from both paths. Verified: layout rendered offscreen via RenderTargetBitmap against the real markup extracted from MainWindow.xaml (9 rows and 27 rows, to confirm the card caps and the list scrolls). The airspace fix itself needs a live run — it cannot be proven headlessly, since it is about OS compositing. 304/304 tests pass, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be
AThraen
added a commit
that referenced
this pull request
Sep 6, 2026
…ally visible (#120) Two waiting states, treated differently on purpose. STARTUP — restore rail A 25-session restore runs ~131s with per-session launch cost swinging 12x (0.6s to 10.9s, measured from crash.log). There is no steady rate to extrapolate from, so an indeterminate spinner reads identically at session 2 and session 22. RestoreRail (2px, under the toolbar) plus a "7 / 25 restoring" pill give the aggregate k-of-N; per-session state stays on the existing placeholder sidebar rows. Restore does NOT block the user — placeholder rows mean you can click into a session the moment it lands — so this stays deliberately out of the way. Nothing modal. The counter advances after the try/catch, not inside it, so a session that fails to restore still moves the rail. Otherwise one bad session strands it short of full, which reads as exactly the hang it is meant to rule out. SHUTDOWN — the overlay was invisible, and that is the real bug Reported as "more like 1 line - thats hard to see. no spinner or anything". The markup looked correct: a Grid sibling with Panel.ZIndex="100" and a Storyboard-rotated arc dead centre. WebView2 is an HwndHost. A native child window is composited by the OS on top of everything WPF renders, and Panel.ZIndex does not enter into it. The overlay was being drawn BEHIND every terminal pane. What reached the user was scrim leaking through the few-pixel gaps between panes — hence a stray line and no spinner, because the spinner sat centred over a pane. The codebase already documents HwndHost for input (#108, three separate call sites). Nobody had written down the output half, which is the more expensive one to rediscover because the code looks right. Fix: collapse TerminalGrid before showing the overlay. Safe — every pane is about to be disposed, and a frozen terminal is worth nothing mid-shutdown. With it visible, it earns being more than a spinner. The board names every session and ticks it off (· pending, ◐ closing, ✓ clean, ⨯ force-disposed), with elapsed time per session, a k/N bar, and a budget bar against ClaudeShutdownBudgetMs. Force-disposed sessions are marked rather than hidden: that is the case a user most wants to see and it used to happen completely silently. ShutdownHint escalates with elapsed time to explain why the wait is long. Explanatory, not jokey — it has to read well on the four-hundredth shutdown, which is the argument against the "be patient" copy I originally sketched. Also here: - Board list scrollbar restyled (scoped to that ScrollViewer). Any setup past ~14 sessions scrolls, so the stock system-coloured bar was on screen for most shutdowns, not an edge case. - Board skipped entirely at zero sessions, so --clean runs don't flash a full-window "0 / 0". - Corrected a stale comment claiming the adaptive config gate is "still used at SHUTDOWN". #117 removed it from both paths. Verified: layout rendered offscreen via RenderTargetBitmap against the real markup extracted from MainWindow.xaml (9 rows and 27 rows, to confirm the card caps and the list scrolls). The airspace fix itself needs a live run — it cannot be proven headlessly, since it is about OS compositing. 304/304 tests pass, 0 warnings. Claude-Session: https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two problems, both found by running it rather than reasoning about it.
1. The adaptive gate overshoots at shutdown too
#111 reverted the config-watching gate on the launch path but kept it at shutdown, on my reasoning that "the machine is quiet at shutdown, so polling is reliable there."
Measurement falsified that:
against a 1000ms cap — 8.7× over, and 56% of the entire shutdown budget spent in a single session. That's what forced the remaining six to be killed with no exit wait.
Same disease as the launch path: when the thread stalls, the gate measures the stall rather than the file. With sessions actively dying, shutdown isn't quiet either. I kept the half of the revert I liked and it had the same bug.
Now a flat
Task.Delay, on both the shutdown loop and the restart path. Recomputing that run flat: 8969ms instead of 15506ms, nothing force-disposed. The gate's typical ~300ms beats a flat 1000ms right up until it doesn't, and the tail is what costs.ClaudeConfigGatenow has no callers and is deleted along with its tests, rather than left as dead code for someone to reintroduce.2. The budget was sized from the wrong data
15s was chosen when the only measurements available were idle sessions exiting in 460–770ms. Busy sessions measure 2.3–4.7s each, so nine of them need roughly 30s — and 15s force-disposed over half the fleet on an ordinary close.
Raised to 30s. A clean exit lets Claude finish writing its config, and
ShutdownOverlayis already on screen explaining the wait. The budget exists to bound a genuinely wedged session, not to hurry a healthy one.Confirmed: the #115
HasExitedfix worksNo
exit=value is anywhere near the 10s cap — max observed 4656ms. Nothing is timing out on a process that already died, which was the entire point of that fix. The remaining cost was genuine slow exits plus the gate overshoot above.Docs
The gate is recorded as gone from both paths, with the falsified reasoning written down.
ClaudeShutdownBudgetMscarries its measurements and a note to re-measureexit=before shrinking it — the summary line alone cannot distinguish "slow exits" from "waits that aren't returning", which is exactly the ambiguity that cost a round here.ClaudeConfigGateTestsdeleted with the gate)🤖 Generated with Claude Code
https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be