[eric] agents: a chat still running when the backend shuts down says so in its transcript, instead of reading like the user's own Stop

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012G8kyALnPjsA7aJFmMBq3R
This commit is contained in:
ciregenz
2026-09-01 19:45:38 -07:00
co-authored by Claude Fable 5.1
parent fc0604155e
commit 6d97692ea7
2 changed files with 49 additions and 1 deletions
@@ -10,7 +10,7 @@ from typing import Optional
from typeguard import typechecked
from backend.apps.agents.core.models import AgentSession
from backend.apps.agents.core.models import AgentSession, Message
from backend.apps.agents.core.ws_manager import ws_manager
from backend.apps.agents.manager.session.session_store import (
load_all_session_data,
@@ -49,6 +49,12 @@ def running_under_test() -> bool:
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
SHUTDOWN_STOP_NOTE = (
"This chat was still running when OpenSwarm's engine shut down, so it stopped here; that was not "
"your Stop. Send a message to continue from where it left off."
)
class SessionPersistence(AgentManagerProtocol):
@typechecked
async def reconcile_on_startup(self) -> None:
@@ -134,6 +140,10 @@ class SessionPersistence(AgentManagerProtocol):
for session_id, session in list(self.sessions.items()):
if session.status in ("running", "waiting_approval"):
session.status = "stopped"
# Say who stopped it. A chat flushed as plain "stopped" reads exactly like the user's own
# Stop, and when something else killed the backend (an agent's pkill, 2026-09-01) the
# user's running work vanished with nothing saying why: silent loss, row 1.
session.messages.append(Message(role="system", content=SHUTDOWN_STOP_NOTE, branch_id=session.active_branch_id))
session.closed_at = None
for req in list(session.pending_approvals):
ws_manager.resolve_approval(req.id, {"behavior": "deny", "message": "Server shutting down"})
+38
View File
@@ -0,0 +1,38 @@
"""A chat that was running when the backend shut down must say so, or the flush reads as the user's Stop.
2026-09-01: an agent's `pkill -f uvicorn` SIGTERMed Eric's production backend twice; every running chat
was persisted as plain "stopped" and nothing in the transcript said why (row 1, silent work loss)."""
import asyncio
from backend.apps.agents.agent_manager import agent_manager
from backend.apps.agents.core.models import AgentSession, Message
from backend.apps.agents.manager.session import SessionPersistence as sp
def p_session(status: str) -> AgentSession:
s = AgentSession(name="t", model="sonnet", status=status)
s.messages.append(Message(role="user", content="build the thing", branch_id="main"))
return s
def test_a_running_chat_gets_the_shutdown_note_and_stops(monkeypatch, tmp_path) -> None:
saved: dict = {}
monkeypatch.setattr(sp, "save_session", lambda sid, doc: saved.__setitem__(sid, doc))
s = p_session("running")
agent_manager.sessions.clear(); agent_manager.sessions[s.id] = s
asyncio.run(agent_manager.persist_all_sessions())
doc = saved[s.id]
assert doc["status"] == "stopped"
last = doc["messages"][-1]
assert last["role"] == "system", "the note is the platform speaking, never the model"
assert "not your Stop" in last["content"] and "Send a message to continue" in last["content"]
def test_a_settled_chat_is_flushed_untouched(monkeypatch) -> None:
saved: dict = {}
monkeypatch.setattr(sp, "save_session", lambda sid, doc: saved.__setitem__(sid, doc))
s = p_session("completed")
agent_manager.sessions.clear(); agent_manager.sessions[s.id] = s
asyncio.run(agent_manager.persist_all_sessions())
doc = saved[s.id]
assert doc["status"] == "completed"
assert doc["messages"][-1]["role"] == "user", "no note on a chat that was not running"