Skip to content

refactor: session handler lifecycle (cancel, busy-gate, exception cleanup)#14

Merged
shoom1 merged 1 commit into
developfrom
refactor/session-lifecycle
May 1, 2026
Merged

refactor: session handler lifecycle (cancel, busy-gate, exception cleanup)#14
shoom1 merged 1 commit into
developfrom
refactor/session-lifecycle

Conversation

@shoom1

@shoom1 shoom1 commented May 1, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the three session-lifecycle findings from review feedback. The handler invocation was previously awaited inline with no task handle, no busy-state, and no exception cleanup; this PR models all three explicitly.

What changed

Gap Before After
Ctrl+C cancelling handler Finished boxes only — handler kept running and could still emit responses Cancels the tracked _current_handler_task (via _user_cancelled_handler flag so the inner cancellation is distinguishable from outer shutdown)
Input while busy Echoed + cleared, but _pending_input was already done → silently lost accept_handler returns False (keeps buffer), shows "Busy — press Ctrl+C to cancel" status, leaves text editable
Handler exception Logged, boxes left active → UI stuck in "thinking" _cleanup_after_handler() finishes any open boxes before re-prompting

How

  • New ThinkingPromptSession._run_handler(handler, text) is the single owner of handler execution: wraps async handlers in asyncio.create_task, stores the task on _current_handler_task, distinguishes user cancellation (_user_cancelled_handler) from outer shutdown (re-raised), and runs _cleanup_after_handler() on every terminal path.
  • The Ctrl+C binding now sets _user_cancelled_handler = True before calling task.cancel(), then finishes boxes / cancels pending input, falling through to app.exit() only when nothing is in flight.
  • input_loop simplifies to await self._run_handler(...); the previous inline try/except for handler exceptions and CancelledError is gone.

Test plan

  • tests/test_session_lifecycle.py — 9 new tests (all initially failing, now green):
    • async-handler tracking, sync-handler skipping, exception cleanup, user-cancel cleanup (CancelledError swallowed), outer-cancel propagation
    • busy-input dropped (returns False, buffer kept, _pending_input not resolved), idle-input delivered
    • Ctrl+C cancels running handler, Ctrl+C exits when idle
  • pytest: 364 passed (was 355, +9)
  • ruff check thinking_prompt/: clean
  • mypy thinking_prompt/: clean
  • CI green

…d exception cleanup

Three coupled gaps in ThinkingPromptSession's input lifecycle:

1. Ctrl+C did not cancel the running handler. The key binding finished
   active thinking boxes and cancelled the pending-input future, but the
   handler coroutine was awaited inline with no task handle, so a long-
   running handler kept executing after the UI signaled "cancel" and
   could still emit a final response.

2. Input submitted while a handler was running was silently dropped.
   accept_handler echoed the text to history, cleared the buffer, and
   tried to deliver it via _pending_input — but _pending_input was
   already done (the previous prompt had resolved it), so the new text
   never reached input_loop. The user lost the command with no signal
   that anything had gone wrong.

3. Handler exceptions left thinking boxes stuck active. The except path
   logged the error but didn't finish boxes the failing handler had
   created; for callers using start_thinking() (manual API) the UI was
   permanently in "thinking" state until restart.

Solution:

- Add ThinkingPromptSession._run_handler(handler, text) that runs the
  handler as a tracked asyncio.Task stored on _current_handler_task.
  CancelledError flagged via _user_cancelled_handler is treated as a
  Ctrl+C (swallowed, boxes finished, loop continues); other Cancellations
  propagate so the input loop can exit cleanly. Exceptions trigger
  _cleanup_after_handler() before being logged.

- Ctrl+C key binding cancels _current_handler_task (after marking
  _user_cancelled_handler), finishes boxes, cancels pending input, and
  only falls through to app.exit() when nothing is in flight.

- accept_handler refuses to deliver input while _current_handler_task
  is running: leaves the buffer intact, sets a "Busy — press Ctrl+C to
  cancel" status hint, and returns False so prompt_toolkit does not
  echo or clear.

- input_loop simplifies to "await self._run_handler(...)"; all retry
  / cleanup / cancellation logic now lives in one place.

Adds tests/test_session_lifecycle.py with 9 tests covering each path:
async/sync handler tracking, exception cleanup, user vs outer cancel,
busy-input drop, and the Ctrl+C key binding (cancels handler, exits
when idle).
@shoom1 shoom1 merged commit f027d32 into develop May 1, 2026
4 checks passed
@shoom1 shoom1 deleted the refactor/session-lifecycle branch May 1, 2026 02:48
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