fix(state): delegate and branch children stop inheriting a compressed parent's gateway routing

_INHERIT_PARENT_ROUTING_SQL copied session_key/chat_id/chat_type/thread_id/user_id/
display_name/origin_json/transport_profile onto ANY child whose parent row ended on
'compression'. A delegate or branch fork of such a parent is not that conversation's
continuation, so it must not take over the route: two live rows holding one routing key
lets peer recovery repoint gateway traffic into a subagent's transcript (#116322).

Exclude children whose `_delegate_from` / `_branched_from` marker names the queried parent
id. Value-match rather than presence: a compression continuation inherits model_config
verbatim (marker included), so a presence check would misclassify it — the same idiom
_NON_CONTINUATION_CHILD_FILTER_SQL already uses.

Cherry-picked from #109699 (Lei-k) minus its unrelated tools/delegate_tool_dispatch.py
nested-dispatch hunk; test relocated to tests/hermes_state/ where the suite now lives.
This commit is contained in:
Hermes Agent
2026-09-19 21:19:57 -07:00
committed by Teknium
parent ecd054539c
commit 3b94ec1be1
2 changed files with 70 additions and 1 deletions

View File

@@ -236,6 +236,15 @@ _INHERIT_PARENT_META_SQL = (
))
+ "\n WHERE id = ? AND parent_session_id IS NOT NULL"
)
# A delegate/branch fork of a row that happens to have ended on compression is still not that
# conversation's continuation. Markers are matched against the QUERIED parent id rather than mere
# presence, for the same reason as _NON_CONTINUATION_CHILD_FILTER_SQL: a continuation inherits its
# parent's model_config verbatim, so presence-matching would misclassify it as a delegate.
_FORK_EDGE_EXCLUSION_SQL = "".join(
f"\n AND COALESCE({_sql_json_extract('model_config', f'$.{marker}')}, '')"
"\n != parent_session_id"
for marker in ("_delegate_from", "_branched_from")
)
_INHERIT_PARENT_ROUTING_SQL = (
"UPDATE sessions\n SET "
+ _INHERIT_SEP.join(_inherit_col_sql(c) for c in (
@@ -248,6 +257,7 @@ _INHERIT_PARENT_ROUTING_SQL = (
" WHERE p.id = sessions.parent_session_id\n"
" AND p.end_reason = 'compression'\n"
" )"
+ _FORK_EDGE_EXCLUSION_SQL
)
@@ -276,7 +286,10 @@ class SessionSessionsMixin:
"""NULL-fill a child's cwd/git/profile from its parent (profile_name only within the same
``agent:<ns>:`` namespace). Gateway routing columns are inherited ONLY by compression forks
(a crash before the gateway re-records the peer would strand the child unroutable); delegate
children must NOT inherit them (peer recovery could repoint traffic into a subagent's session)."""
and branch children must NOT inherit them (peer recovery could repoint traffic into a
subagent's session), including when their parent row itself ended on compression — a long
batch outlives its coordinator's rotation, and two live rows holding one routing key is the
shape reported in #92859."""
conn.execute(_INHERIT_PARENT_META_SQL, (session_id,))
conn.execute(_INHERIT_PARENT_ROUTING_SQL, (session_id,))