Skip to content

Interrupting a gateway turn crashes with ConnectionError when the connection is already lost #1505

Description

@lihongguang0014

Summary

Pressing Ctrl+C to interrupt an in-flight gateway turn can crash the turn with an unexpected ConnectionError instead of cancelling it gracefully, breaking the REPL's "cancel and re-prompt" contract.

Reproduction

  1. Start a CLI chat session connected to the gateway.
  2. Let the gateway connection drop (gateway restart, network blip) so GatewayClient._ws is gone / _connection_error is set.
  3. Press Ctrl+C while the turn is still awaiting stream events.

Root cause

In src/opensquilla/cli/chat/turn_stream.py (gateway stream loop, around line 1182), the cancellation handler awaits client.abort_session(...) inside the except (KeyboardInterrupt, asyncio.CancelledError) block:

except (KeyboardInterrupt, asyncio.CancelledError):
    stream_deps.cancel_clearer()
    await client.abort_session(session_key)   # <-- may raise
    cancelled = True

GatewayClient._call() raises ConnectionError("Gateway connection lost; ...") when the connection is already gone (src/opensquilla/cli/gateway_client.py, _call error path, around lines 634-639).

An exception raised inside an except block escapes it — the sibling except Exception handler can no longer catch it — so the turn never reaches TurnResult(cancelled=True), renderer_finalize is skipped, and the REPL surfaces an unexpected ConnectionError (in the TUI, violating the invariant documented in tests/unit/cli/tui/test_native_input.py that KeyboardInterrupt must cancel the turn and re-prompt, never exit).

Interrupting a turn whose connection just died is exactly the scenario where users press Ctrl+C, so this is easy to hit in practice.

Suggested fix

Wrap the abort_session call in try/except Exception inside the cancellation handler: the turn is already cancelled locally, and the abort RPC is best-effort:

except (KeyboardInterrupt, asyncio.CancelledError):
    stream_deps.cancel_clearer()
    try:
        await client.abort_session(session_key)
    except Exception:
        # The gateway connection may already be gone (that is
        # often why the user interrupted). The turn is still
        # cancelled locally so the REPL can re-prompt.
        pass
    cancelled = True

Regression test

test_gateway_stream_interrupt_tolerates_abort_failure (in tests/test_cli/test_chat_cmd.py) — send_message raises KeyboardInterrupt, abort_session raises ConnectionError; asserts TurnResult.cancelled is True.

Verified: the new test fails on the unfixed code and passes with the fix; all 63 tests in tests/test_cli/test_chat_cmd.py pass.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    has-linked-prAn open pull request is linked to this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions