mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-26 01:01:40 +00:00
fix(state): orphan children instead of cascade-deleting in prune/delete (#6513)
prune_sessions and delete_session only handled direct children when satisfying the parent_session_id FK constraint. Multi-level chains (A -> B -> C) caused IntegrityError because deleting B while C still referenced it was blocked by the FK. Fix: NULL out parent_session_id for any session whose parent is about to be deleted. This orphans children instead of cascade-deleting them, which also respects the prune retention window — newer child sessions are no longer deleted just because an ancestor is old. Reported by Aaryan2304 in PR #6463.
This commit is contained in:
parent
851857e413
commit
a94099908a
2 changed files with 99 additions and 25 deletions
|
|
@ -663,6 +663,84 @@ class TestPruneSessions:
|
|||
assert db.get_session("old_cli") is None
|
||||
assert db.get_session("old_tg") is not None
|
||||
|
||||
def test_prune_with_multilevel_chain(self, db):
|
||||
"""Pruning old sessions orphans newer children instead of crashing on FK."""
|
||||
old_ts = time.time() - 200 * 86400
|
||||
recent_ts = time.time() - 10 * 86400
|
||||
|
||||
# Chain: A (old) -> B (old) -> C (recent) -> D (recent)
|
||||
db.create_session(session_id="A", source="cli")
|
||||
db.end_session("A", end_reason="compressed")
|
||||
db.create_session(session_id="B", source="cli", parent_session_id="A")
|
||||
db.end_session("B", end_reason="compressed")
|
||||
db.create_session(session_id="C", source="cli", parent_session_id="B")
|
||||
db.end_session("C", end_reason="compressed")
|
||||
db.create_session(session_id="D", source="cli", parent_session_id="C")
|
||||
db.end_session("D", end_reason="done")
|
||||
|
||||
# Backdate A and B to be old; C and D stay recent
|
||||
for sid, ts in [("A", old_ts), ("B", old_ts), ("C", recent_ts), ("D", recent_ts)]:
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET started_at = ? WHERE id = ?", (ts, sid)
|
||||
)
|
||||
db._conn.commit()
|
||||
|
||||
# Should not raise IntegrityError
|
||||
pruned = db.prune_sessions(older_than_days=90)
|
||||
assert pruned == 2 # only A and B
|
||||
assert db.get_session("A") is None
|
||||
assert db.get_session("B") is None
|
||||
# C and D survive, C is orphaned (parent_session_id NULL)
|
||||
c = db.get_session("C")
|
||||
assert c is not None
|
||||
assert c["parent_session_id"] is None
|
||||
d = db.get_session("D")
|
||||
assert d is not None
|
||||
assert d["parent_session_id"] == "C"
|
||||
|
||||
def test_prune_entire_old_chain(self, db):
|
||||
"""All sessions in a chain are old — entire chain is pruned."""
|
||||
old_ts = time.time() - 200 * 86400
|
||||
|
||||
db.create_session(session_id="X", source="cli")
|
||||
db.end_session("X", end_reason="compressed")
|
||||
db.create_session(session_id="Y", source="cli", parent_session_id="X")
|
||||
db.end_session("Y", end_reason="compressed")
|
||||
db.create_session(session_id="Z", source="cli", parent_session_id="Y")
|
||||
db.end_session("Z", end_reason="done")
|
||||
|
||||
for sid in ("X", "Y", "Z"):
|
||||
db._conn.execute(
|
||||
"UPDATE sessions SET started_at = ? WHERE id = ?", (old_ts, sid)
|
||||
)
|
||||
db._conn.commit()
|
||||
|
||||
pruned = db.prune_sessions(older_than_days=90)
|
||||
assert pruned == 3
|
||||
for sid in ("X", "Y", "Z"):
|
||||
assert db.get_session(sid) is None
|
||||
|
||||
|
||||
class TestDeleteSessionOrphansChildren:
|
||||
def test_delete_orphans_children(self, db):
|
||||
"""Deleting a parent session orphans its children."""
|
||||
db.create_session(session_id="parent", source="cli")
|
||||
db.create_session(session_id="child", source="cli", parent_session_id="parent")
|
||||
db.create_session(session_id="grandchild", source="cli", parent_session_id="child")
|
||||
|
||||
# Should not raise IntegrityError
|
||||
result = db.delete_session("parent")
|
||||
assert result is True
|
||||
assert db.get_session("parent") is None
|
||||
# Child is orphaned, not deleted
|
||||
child = db.get_session("child")
|
||||
assert child is not None
|
||||
assert child["parent_session_id"] is None
|
||||
# Grandchild is untouched
|
||||
grandchild = db.get_session("grandchild")
|
||||
assert grandchild is not None
|
||||
assert grandchild["parent_session_id"] == "child"
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Schema and WAL mode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue