Skip to content

Map Ctrl-C to exit code 130 (cancel) instead of 0#202

Merged
alexkroman merged 1 commit into
mainfrom
claude/adoring-mayer-icbqmp
Jun 16, 2026
Merged

Map Ctrl-C to exit code 130 (cancel) instead of 0#202
alexkroman merged 1 commit into
mainfrom
claude/adoring-mayer-icbqmp

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Summary

This change establishes a consistent exit code convention for user interrupts (Ctrl-C / SIGTERM) across all interactive and streaming commands. Previously, Ctrl-C was treated as a clean success (exit 0), which made it impossible for callers to distinguish between a normal completion and an interrupt. Now all Ctrl-C paths exit with code 130 (the conventional Unix "cancelled by SIGINT" code), while maintaining clean shutdown behavior.

Key Changes

  • Core interrupt handling: Added CANCELLED_EXIT_CODE = 130 constant in aai_cli/core/errors.py as the single source of truth for cancel exit codes.

  • Telemetry tracking: Updated aai_cli/core/telemetry.py to:

    • Map exit code 130 to "cancelled" outcome (distinct from generic "error")
    • Treat raw KeyboardInterrupt exceptions as "cancelled" rather than "internal_error"
    • Preserve the distinction so interrupts don't inflate crash metrics
  • Command execution: Refactored aai_cli/app/context.py to:

    • Extract telemetry/update-check wrapping into _run_body() helper
    • Catch KeyboardInterrupt in run_command() and map it to typer.Exit(code=130)
    • Preserve the existing deferred-config path that skips telemetry
  • Interactive commands: Updated all interactive/streaming commands to raise typer.Exit(code=130) on Ctrl-C instead of returning cleanly:

    • aai_cli/commands/llm/_exec.py (follow mode)
    • aai_cli/commands/dictate/_exec.py
    • aai_cli/commands/agent/_exec.py
    • aai_cli/commands/agent_cascade/_exec.py
    • aai_cli/commands/share/_exec.py
    • aai_cli/commands/webhooks/_listen.py
    • aai_cli/streaming/session.py (batch streaming)
    • aai_cli/init/runner.py (dev server)
  • Tests: Updated all test expectations to verify exit code 130 on Ctrl-C, with clarified comments explaining the cancel semantics.

Implementation Details

  • The change preserves all existing cleanup behavior (renderer closes, tunnels tear down, etc.) — only the exit code changes.
  • Commands with no interactive handler (raw KeyboardInterrupt reaching run_command) now exit 130 instead of Click's default exit 1.
  • Telemetry correctly identifies 130 as "cancelled" so it doesn't count toward crash rates.
  • The config path command (which tolerates unreadable config) continues to skip telemetry/update-check wrappers as before.
  • Callers can now reliably use exit code 130 to detect interrupts in shell scripts: assembly stream && next_command will not run next_command if the user pressed Ctrl-C.

https://claude.ai/code/session_01BRiB7KpAgb833DkxHh5ntc

The CLI's documented exit-code contract (REFERENCE.md / errors.py) already
promised "130 — cancelled with Ctrl-C", but the interactive commands swallowed
KeyboardInterrupt and returned 0. That made `assembly stream && next` run `next`
after a Ctrl-C, and broke composition with make/just/CI, which read exit 0 as
success.

Map a cancel to its conventional Unix code (128 + SIGINT) in one place:
run_command now catches KeyboardInterrupt and exits 130, and each interactive
handler (stream/agent/agent-cascade/dictate/llm --follow/share/webhooks listen
and the init/dev server runner) raises typer.Exit(130) after its clean "Stopped."
flush rather than returning 0. SIGTERM, which core.signals already routes through
the same clean-stop path, rides along to 130. The transcript is still saved and
"Stopped." still prints — only the misleading exit 0 goes away; `q` in dictate
still finishes with 0.

Telemetry records the cancel as a new "cancelled" outcome (exit 130) so a Ctrl-C
of a long-running stream/agent session doesn't inflate the crash rate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRiB7KpAgb833DkxHh5ntc
@alexkroman alexkroman enabled auto-merge June 16, 2026 23:18
@alexkroman alexkroman added this pull request to the merge queue Jun 16, 2026
Merged via the queue into main with commit 41b9c5e Jun 16, 2026
19 checks passed
@alexkroman alexkroman deleted the claude/adoring-mayer-icbqmp branch June 16, 2026 23:26
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