Pins the reason the exclusion matches the marker's VALUE against the parent id instead of its presence: a continuation copies model_config verbatim, so a `_branched_from` marker rides along. A presence check (#116377's variant) would leave the continuation unroutable — exactly the shape this test fails on against that branch. Salvages #109699 (@Lei-k). Supersedes #116377 (@strzhao). Co-authored-by: strzhao <strzhao@users.noreply.github.com>
76 lines
3.3 KiB
Python
76 lines
3.3 KiB
Python
"""A delegate/branch child must never inherit its parent's gateway routing columns.
|
|
|
|
``_inherit_parent_session_metadata`` states the rule in its own docstring — routing columns are
|
|
inherited ONLY across a compression fork, because "peer recovery could repoint traffic into a
|
|
subagent's session" — but the SQL gated on the PARENT's ``end_reason`` alone. A delegate child
|
|
whose gateway parent had already rotated on compression (long batch, queued child, detached unit)
|
|
therefore took the chat's ``session_key``/``chat_id``/``user_id``, leaving two live rows holding one
|
|
routing key (NousResearch/hermes-agent#116322, first reported as #92859).
|
|
|
|
Real ``SessionDB`` on a temp path, no mocks: the contract asserted here is the RELATIONSHIP between
|
|
the two child kinds — a compression continuation keeps inheriting, a delegate/branch fork does not.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from hermes_state import SessionDB
|
|
|
|
ROUTING_COLUMNS = ("session_key", "chat_id", "chat_type", "thread_id", "user_id")
|
|
|
|
|
|
@pytest.fixture()
|
|
def db(tmp_path):
|
|
session_db = SessionDB(db_path=tmp_path / "state.db")
|
|
try:
|
|
yield session_db
|
|
finally:
|
|
session_db.close()
|
|
|
|
|
|
def _compressed_gateway_parent(db: SessionDB) -> dict:
|
|
"""A gateway conversation that rotated on compression, i.e. the one case that inherits."""
|
|
db.create_session(
|
|
"parent", source="telegram", session_key="agent:main:telegram:dm:42",
|
|
chat_id="42", chat_type="dm", thread_id="7", user_id="u1",
|
|
)
|
|
db.end_session("parent", "compression")
|
|
return db.get_session("parent")
|
|
|
|
|
|
@pytest.mark.parametrize("marker", ["_delegate_from", "_branched_from"])
|
|
def test_delegate_and_branch_children_do_not_take_over_the_parent_route(db: SessionDB, marker: str) -> None:
|
|
parent = _compressed_gateway_parent(db)
|
|
|
|
db.create_session(
|
|
"worker", source="subagent", parent_session_id="parent", model_config={marker: "parent"},
|
|
)
|
|
db.create_session("continuation", source="telegram", parent_session_id="parent")
|
|
|
|
worker, continuation = db.get_session("worker"), db.get_session("continuation")
|
|
for column in ROUTING_COLUMNS:
|
|
assert parent[column], f"fixture must seed {column}"
|
|
# The continuation IS the conversation; the worker is an internal transcript.
|
|
assert continuation[column] == parent[column], column
|
|
assert worker[column] is None, column
|
|
|
|
|
|
def test_compression_continuation_of_a_branch_child_keeps_inheriting(db: SessionDB) -> None:
|
|
"""A continuation copies model_config verbatim — the branch marker comes along — so the
|
|
exclusion must match the marker's VALUE against the parent id, not its mere presence."""
|
|
db.create_session("root", source="telegram")
|
|
db.create_session(
|
|
"branch", source="telegram", parent_session_id="root", model_config={"_branched_from": "root"},
|
|
session_key="agent:main:telegram:dm:42", chat_id="42", chat_type="dm", user_id="u1",
|
|
)
|
|
db.end_session("branch", "compression")
|
|
|
|
db.create_session(
|
|
"continuation", source="telegram", parent_session_id="branch", model_config={"_branched_from": "root"},
|
|
)
|
|
|
|
continuation = db.get_session("continuation")
|
|
assert continuation["session_key"] == "agent:main:telegram:dm:42"
|
|
assert continuation["chat_id"] == "42" and continuation["user_id"] == "u1"
|