hermes: close stale API worker sessions

This commit is contained in:
jenkins 2026-08-15 14:20:12 -03:00
parent 6f459dfc63
commit 6fd7b9bbba
2 changed files with 63 additions and 0 deletions

View File

@ -37,6 +37,28 @@ TRIAGE_MESSAGE_PREFIXES = (
)
def close_stale_api_workers(connection: sqlite3.Connection) -> int:
"""Close parent-linked API runs that survived a prior gateway lifetime."""
columns = {
str(row[1])
for row in connection.execute("PRAGMA table_info(sessions)").fetchall()
}
if not {"ended_at", "end_reason"}.issubset(columns):
return 0
cursor = connection.execute(
"""
UPDATE sessions
SET ended_at = ?,
end_reason = 'api_run_recovered_stale'
WHERE source = 'api_server'
AND parent_session_id IS NOT NULL
AND ended_at IS NULL
""",
(time.time(),),
)
return cursor.rowcount
def _triage_title(message: str, session_id: str) -> str:
"""Create a concise label from Ariadne's stable incident contract."""
match = re.search(r"(?:for|Analyze) incident ([^\s.]+)(?:\.|\s)", message)
@ -98,6 +120,7 @@ def migrate(path: Path = STATE_DB, *, group_triage: bool = False) -> int:
return 0
changed = 0
with sqlite3.connect(path) as connection:
changed += close_stale_api_workers(connection)
parent = connection.execute(
"SELECT id FROM sessions WHERE id = ?", (LEGACY_CASSANDRA_PARENT,)
).fetchone()

View File

@ -1186,6 +1186,46 @@ def test_automated_triage_sessions_are_grouped_without_touching_interactive_runs
)
def test_stale_parent_linked_api_workers_close_on_startup(tmp_path: Path):
"""A previous gateway lifetime cannot leave phantom active workers."""
module_path = HERMES / "scripts" / "migrate_api_session_lineage.py"
spec = importlib.util.spec_from_file_location("close_stale_api_workers", module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
database = tmp_path / "state.db"
with sqlite3.connect(database) as connection:
connection.execute(
"CREATE TABLE sessions (id TEXT PRIMARY KEY, source TEXT, "
"parent_session_id TEXT, title TEXT, started_at REAL, ended_at REAL, "
"end_reason TEXT, archived INTEGER DEFAULT 0)"
)
connection.executemany(
"INSERT INTO sessions "
"(id, source, parent_session_id, started_at, ended_at, end_reason) "
"VALUES (?, ?, ?, 1, ?, ?)",
(
("stale", "api_server", "parent", None, None),
("root", "api_server", None, None, None),
("finished", "api_server", "parent", 2.0, "api_run_completed"),
("interactive", "tui", "parent", None, None),
),
)
assert module.migrate(database) == 1
assert module.migrate(database) == 0
with sqlite3.connect(database) as connection:
rows = connection.execute(
"SELECT id, ended_at, end_reason FROM sessions ORDER BY id"
).fetchall()
by_id = {row[0]: row[1:] for row in rows}
assert by_id["stale"][0] is not None
assert by_id["stale"][1] == "api_run_recovered_stale"
assert by_id["root"] == (None, None)
assert by_id["finished"] == (2.0, "api_run_completed")
assert by_id["interactive"] == (None, None)
def test_switchyard_brokers_and_native_claude_lane_use_the_right_images():
"""Thin brokers stay small while native Claude runs beside owner auth."""
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-switchyard-brokers").read_text()