From 952a885fbfa2b7c4f12791bfeead6d25bb361036 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:59:05 -0700 Subject: [PATCH] fix(gateway): /stop no longer resets the session (#9224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /stop was calling suspend_session() which marked the session for auto-reset on the next message. This meant users lost their conversation history every time they stopped a running agent — especially painful for untitled sessions that can't be resumed by name. Now /stop just interrupts the agent and cleans the session lock. The session stays intact so users can continue the conversation. The suspend behavior was introduced in #7536 to break stuck session resume loops on gateway restart. That case is already handled by suspend_recently_active() which runs at gateway startup, so removing it from /stop doesn't regress the original fix. --- gateway/run.py | 19 ++++++------------- tests/gateway/test_session_race_guard.py | 6 ++---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index afc5aa035e..7b96224858 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2546,11 +2546,8 @@ class GatewayRunner: self._pending_messages.pop(_quick_key, None) if _quick_key in self._running_agents: del self._running_agents[_quick_key] - # Mark session suspended so the next message starts fresh - # instead of resuming the stuck context (#7536). - self.session_store.suspend_session(_quick_key) - logger.info("HARD STOP for session %s — suspended, session lock released", _quick_key[:20]) - return "⚡ Force-stopped. The session is suspended — your next message will start fresh." + logger.info("STOP for session %s — agent interrupted, session lock released", _quick_key[:20]) + return "⚡ Stopped. You can continue this session." # /reset and /new must bypass the running-agent guard so they # actually dispatch as commands instead of being queued as user @@ -4120,9 +4117,7 @@ class GatewayRunner: only through normal command dispatch (no running agent) or as a fallback. Force-clean the session lock in all cases for safety. - When there IS a running/pending agent, the session is also marked - as *suspended* so the next message starts a fresh session instead - of resuming the stuck context (#7536). + The session is preserved so the user can continue the conversation. """ source = event.source session_entry = self.session_store.get_or_create_session(source) @@ -4133,17 +4128,15 @@ class GatewayRunner: # Force-clean the sentinel so the session is unlocked. if session_key in self._running_agents: del self._running_agents[session_key] - self.session_store.suspend_session(session_key) - logger.info("HARD STOP (pending) for session %s — suspended, sentinel cleared", session_key[:20]) - return "⚡ Force-stopped. The agent was still starting — your next message will start fresh." + logger.info("STOP (pending) for session %s — sentinel cleared", session_key[:20]) + return "⚡ Stopped. The agent hadn't started yet — you can continue this session." if agent: agent.interrupt("Stop requested") # Force-clean the session lock so a truly hung agent doesn't # keep it locked forever. if session_key in self._running_agents: del self._running_agents[session_key] - self.session_store.suspend_session(session_key) - return "⚡ Force-stopped. Your next message will start a fresh session." + return "⚡ Stopped. You can continue this session." else: return "No active task to stop." diff --git a/tests/gateway/test_session_race_guard.py b/tests/gateway/test_session_race_guard.py index c9e226b67a..fcfaba784d 100644 --- a/tests/gateway/test_session_race_guard.py +++ b/tests/gateway/test_session_race_guard.py @@ -242,9 +242,7 @@ async def test_stop_during_sentinel_force_cleans_session(): stop_event = _make_event(text="/stop") result = await runner._handle_message(stop_event) assert result is not None, "/stop during sentinel should return a message" - assert "force-stopped" in result.lower() or "unlocked" in result.lower() - - # Sentinel must be cleaned up + assert "stopped" in result.lower() assert session_key not in runner._running_agents, ( "/stop must remove sentinel so the session is unlocked" ) @@ -291,7 +289,7 @@ async def test_stop_hard_kills_running_agent(): # Must return a confirmation assert result is not None - assert "force-stopped" in result.lower() or "unlocked" in result.lower() + assert "stopped" in result.lower() # ------------------------------------------------------------------