Skip to content

feat: warn and block a second Ctrl-C during eval cleanup#1972

Merged
mikasenghaas merged 4 commits into
mainfrom
feat/graceful-interrupt
Jul 11, 2026
Merged

feat: warn and block a second Ctrl-C during eval cleanup#1972
mikasenghaas merged 4 commits into
mainfrom
feat/graceful-interrupt

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

  • On the first Ctrl-C (SIGINT) or SIGTERM during eval, warn that cleanup is in progress and raise KeyboardInterrupt so asyncio unwinds each rollout's finally — the path that tears down containers/sandboxes and any worker pool the run spawned.
  • Swallow any further shutdown signal during that window, so an impatient second Ctrl-C can't cut cleanup short and orphan those resources. A genuinely stuck run is still killable with SIGKILL.
  • Works in both display modes:
    • non-rich: the handler echoes the notice to stderr, alongside the teardown logs streaming to the console.
    • rich: the dashboard shows a yellow, all-lowercase "interrupted — cleaning up" line as a footer under the rollouts (beside the --push line; console logging is silenced in rich mode), and the live refresh loop keeps repainting through teardown instead of freezing on the last frame.
  • The CLI now exits on the conventional 130 code without dumping a KeyboardInterrupt traceback; partial results remain on disk (traces are persisted incrementally).
  • Replaces the previous SIGTERM-only remap with a single verifiers/v1/utils/interrupt.py handler covering SIGINT + SIGTERM.

Related

Overlaps with #1925 (open, follow-up to the merged #1920). That PR surfaces the teardown drain from the runtime layer (Runtime.stop aggregate warning + run_shielded) and explicitly leaves the --rich dashboard surfacing as future work — which this PR implements.

Note the deliberate divergence on the second Ctrl-C: #1925 makes a second press abort immediately ("never subordinate a user abort"), whereas this PR blocks it so cleanup always runs to completion. These two policies are mutually exclusive and should be reconciled before both land.

Verification

Signal handler and rendering (unit-level):

  • First signal warns + raises KeyboardInterrupt, cleaning_up() flips true, second signal is swallowed with a "ctrl-c ignored" notice (rich mode: no stderr spam, footer shows instead).
  • Dashboard _interrupt_footer() is hidden before Ctrl-C and renders below the rollouts (yellow, lowercase) after.
  • live_view keeps refreshing during a shielded teardown (simulating asyncio's cancel-all-tasks on Ctrl-C) and terminates cleanly without hanging.

End-to-end (real eval CLI, rollout hung on an unresponsive endpoint so teardown is in-flight):

  • First SIGINTinterrupted — cleaning up, please wait..., teardown runs to completion (interception down).
  • Second SIGINT mid-teardown does not abort — the process finishes cleanup and exits 130 (verified the handler runs on the 2nd/3rd signal and teardown completes uninterrupted).

Breaking

None. --server and legacy paths are unaffected (both run non-rich); server + rich remains rejected at config validation.

🤖 Generated with Claude Code


Note

Medium Risk
Changes process-wide signal handling and shutdown semantics (second Ctrl-C no longer kills immediately), which affects container/sandbox teardown reliability but is limited to the eval CLI paths that call install_interrupt.

Overview
Introduces verifiers/v1/utils/interrupt.py so the eval CLI handles SIGINT and SIGTERM in one place: the first signal logs a warning, sets a global cleaning_up flag, and raises KeyboardInterrupt so asyncio can run rollout teardown; later signals during cleanup are ignored (with a “ctrl-c ignored” warning in non-rich mode).

The eval entrypoint calls install_interrupt() instead of remapping only SIGTERM, catches KeyboardInterrupt, and exits with code 130 without a traceback. Partial traces on disk are unchanged.

In rich mode, _interrupt_footer() shows a yellow cleanup line under the rollouts (alongside --push status), and live_view keeps repainting while cleaning_up() is true even when Ctrl-C cancels the refresh task—so the dashboard does not freeze mid-teardown.

Reviewed by Cursor Bugbot for commit 7e8df73. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Warn and block a second Ctrl-C during eval cleanup

  • Adds interrupt.py with install_interrupt() and cleaning_up() helpers; the first SIGINT/SIGTERM sets a cleanup flag and raises KeyboardInterrupt, subsequent signals during cleanup are silently ignored.
  • Replaces ad-hoc SIGTERM handling in eval/main.py with install_interrupt() and catches KeyboardInterrupt to exit with code 130.
  • Adds a yellow cleanup footer to the eval dashboard in dashboard/eval.py that displays once shutdown starts, with pagination adjusted to account for the extra footer height.
  • Keeps the live dashboard refreshing in dashboard/base.py during Ctrl-C so cleanup notices remain visible until teardown begins.
  • Behavioral Change: process now exits with code 130 after a clean shutdown; repeated Ctrl-C during cleanup is ignored rather than escalating.

Macroscope summarized 7e8df73.

…ting cleanup

The first Ctrl-C/SIGTERM now warns that cleanup is running and raises
KeyboardInterrupt so asyncio unwinds each rollout's `finally` (container/sandbox
teardown). Further shutdown signals during that window are swallowed so an
impatient second Ctrl-C can't cut cleanup short and orphan those resources.

Works in both modes: non-rich echoes the notice to stderr; rich renders it as a
banner in the dashboard, whose refresh loop now keeps painting through teardown.
The CLI exits on the conventional 130 without a traceback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@macroscopeapp

macroscopeapp Bot commented Jul 11, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

This PR modifies signal handling to swallow subsequent Ctrl-C presses during cleanup, preventing forced exit behavior. While the intent is defensive (preventing orphaned containers), changing interrupt handling semantics is a significant runtime behavior change that warrants human review.

You can customize Macroscope's approvability policy. Learn more.

mikasenghaas and others added 3 commits July 10, 2026 19:44
…dashboard

Move the graceful-shutdown notice from a banner atop the dashboard to a footer
under the rollouts, beside the --push line, styled yellow and all-lowercase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use the standard library logger (intercepted into loguru) instead of a raw
stderr write, so the notice is timestamped and recorded in eval.log in both
modes. In rich mode it lands in the log file only (the dashboard footer is the
on-screen echo), so the `rich` param is no longer needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mikasenghaas mikasenghaas requested a review from samsja July 11, 2026 03:00
@mikasenghaas mikasenghaas merged commit 67e77f6 into main Jul 11, 2026
13 checks passed
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.

2 participants