A /stop sets _interrupt_requested on the session's cached agent, but the
flag is only cleared by the turn finalizer. When the stopped run is hung
or still draining, the flag survives the forced lock release and the
session's NEXT user message is killed at the top of the tool loop
(conversation_loop.py interrupt check): the run completes with
interrupted=True, api_calls=0 and an empty response, which
_normalize_empty_agent_response passed through as pure silence — the
user's message was swallowed with no trace except a
'response ready: ... api_calls=0 response=0 chars' log line.
Two-layer fix:
- _interrupt_and_clear_session now evicts the cached agent whenever it
releases the running state. The next message rebuilds the agent from
session history (mirroring the /new and /model paths), while the old
agent object keeps its interrupt flag so a hung drain still dies when
it unblocks. This intentionally does NOT clear the flag in place:
turn_context deliberately preserves a pending interrupt across turn
start (it carries interrupt-message delivery), and clearing it could
revive a hung run the user just stopped.
- _normalize_empty_agent_response distinguishes a drain from a swallowed
turn: an interrupted run that did work (api_calls > 0) stays silent as
before (deliberate stop/steer; queued messages are delivered by the
recursive drain inside _run_agent), but an interrupted run with ZERO
api_calls never processed the user's message at all and now surfaces a
'send it again' notice instead of nothing.
Same silent-delivery class as a1f76ba7e (#29346), which covered the
extract-stripped case; regression tests added next to that coverage.
Fixes#44212