3a83b6bc5d
Detect a routing key whose session is already ended in state.db (end_reason set) inside get_or_create_session and drop the stale entry instead of silently routing the message into a closed session. Previously the only runtime cleanup of sessions.json was the startup _prune_stale_sessions_locked (#52808/#54138), which requires a restart. A session ended while the gateway stays alive — any path that finalizes the DB row without clearing sessions.json — left a live routing key pointing at a closed session. get_or_create_session never consulted end_reason, so it returned that stale entry and every subsequent message was silently dropped (no log, no error, no response) until the next restart. This is the live-gateway variant of #52804/FM9, which needed an actual gateway crash. The guard drops the stale entry and falls through to _recover_session_from_db, which reopens agent_close-ended rows and resumes the SAME session_id (transcript preserved); if the row ended for a non-recoverable reason (e.g. /new) it correctly starts a fresh session. A warning is logged so the event is visible (the field incident reported zero log output). Adds tests/gateway/test_session_store_runtime_stale_guard.py covering the _is_session_ended_in_db helper and the end-to-end routing self-heal (recover-vs-fresh, live-entry untouched, stale-wins-over-suspended, force_new short-circuit). Closes #54878. Co-authored-by: David Gutowsky <david.gutowsky@gmail.com>