Skip to content

fix(shutdown): flat post-exit pause and a budget sized from measurement - #117

Merged
AThraen merged 1 commit into
mainfrom
fix/shutdown-flat-settle-and-budget
Sep 6, 2026
Merged

fix(shutdown): flat post-exit pause and a budget sized from measurement#117
AThraen merged 1 commit into
mainfrom
fix/shutdown-flat-settle-and-budget

Conversation

@AThraen

@AThraen AThraen commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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:

SHUTDOWN 'SiteAnalysis': exit=4656ms  cfgSettle=8731ms

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.

ClaudeConfigGate now 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 ShutdownOverlay is already on screen explaining the wait. The budget exists to bound a genuinely wedged session, not to hurry a healthy one.

Confirmed: the #115 HasExited fix works

No 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. ClaudeShutdownBudgetMs carries its measurements and a note to re-measure exit= 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.

Check Result
Unit tests 304/304 (10 fewer — ClaudeConfigGateTests deleted with the gate)
App + Tests build 0 errors, 0 warnings

🤖 Generated with Claude Code

https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be

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
AThraen merged commit d48e36d into main Sep 6, 2026
1 check passed
@AThraen
AThraen deleted the fix/shutdown-flat-settle-and-budget branch September 6, 2026 12:51
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>
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.

1 participant