hermes: close stale API worker sessions
This commit is contained in:
parent
6f459dfc63
commit
6fd7b9bbba
@ -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:
|
def _triage_title(message: str, session_id: str) -> str:
|
||||||
"""Create a concise label from Ariadne's stable incident contract."""
|
"""Create a concise label from Ariadne's stable incident contract."""
|
||||||
match = re.search(r"(?:for|Analyze) incident ([^\s.]+)(?:\.|\s)", message)
|
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
|
return 0
|
||||||
changed = 0
|
changed = 0
|
||||||
with sqlite3.connect(path) as connection:
|
with sqlite3.connect(path) as connection:
|
||||||
|
changed += close_stale_api_workers(connection)
|
||||||
parent = connection.execute(
|
parent = connection.execute(
|
||||||
"SELECT id FROM sessions WHERE id = ?", (LEGACY_CASSANDRA_PARENT,)
|
"SELECT id FROM sessions WHERE id = ?", (LEGACY_CASSANDRA_PARENT,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|||||||
@ -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():
|
def test_switchyard_brokers_and_native_claude_lane_use_the_right_images():
|
||||||
"""Thin brokers stay small while native Claude runs beside owner auth."""
|
"""Thin brokers stay small while native Claude runs beside owner auth."""
|
||||||
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-switchyard-brokers").read_text()
|
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-switchyard-brokers").read_text()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user