Skip to content

fix(state): stop inheriting gateway routing columns into delegate children - #116377

Closed
strzhao wants to merge 2 commits into
NousResearch:mainfrom
strzhao:forge/116322-delegate-routing-leak
Closed

strzhao wants to merge 2 commits into
NousResearch:mainfrom
strzhao:forge/116322-delegate-routing-leak

Conversation

@strzhao

@strzhao strzhao commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #116322

_INHERIT_PARENT_ROUTING_SQL (hermes_state_sessions.py:239-249) copies the parent session's gateway routing columns (source/session_key/chat_id/chat_type/user_id) into any child whose parent ended with end_reason='compression' — including delegate/subagent children marked by model_config._delegate_from. After a peer failure/recovery, such a child can silently repoint real user traffic into the subagent's session.

The code base already states the intended contract three times; only the inheritance SQL misses it:

  • the _INHERIT_PARENT_ROUTING_SQL docstring: "routing columns are inherited ONLY by compression forks" (:277);
  • the two sibling routing queries (:440-446, :478) both explicitly exclude _delegate_from children;
  • this inheritance query was the one SQL path that did not.

Change

One focus: add AND _delegate_from IS NULL to the inheritance SQL, via the existing json_valid-guarded _sql_json_extract helper (malformed model_config falls back safely), mirroring the sibling queries' shape. Compression forks keep inheriting; real delegate children stop inheriting. Nothing else changes.

Tests

Two invariant tests (per the 1-2 test norm):

  1. test_delegate_child_does_not_inherit_gateway_routing_columns — delegate child (model_config._delegate_from) gets session_key/chat_id/chat_type/user_id = NULL (RED before the fix, GREEN after);
  2. test_compression_fork_still_inherits_gateway_routing_columns — compression fork keeps inheriting all four routing columns (GREEN before and after; guards against over-narrowing).

Mutation self-check: replaying the pre-fix logic turns test 1 red (exit=1), restoring the fix turns it green (exit=0). Full tests/hermes_state/test_hermes_state.py: 283 passed / 2 skipped (skips pre-existing); sibling suites (delegate_cascade / compression_lineage_guard / profile_stamp / orphan_gateway_repair): 43 passed.

Test plan

  • pytest tests/hermes_state/test_hermes_state.py — 283 passed, 2 skipped (pre-existing skips)
  • sibling suites — 43 passed
  • mutation check: old-logic replay red → restore green

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/delegate Subagent delegation area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 19, 2026
…rch#116322)

Red-team review of NousResearch#116377 found the exclusion clause covered only the
_delegate_from marker while the same defect class reproduces through the
/branch path: tui_gateway._persist_branch builds the child row with
model_config={"_branched_from": parent_key} and parent_session_id, passing
no routing columns, so the inheritance gate is the only source of its
session_key/chat_id — the same two-live-rows-one-key shape as the delegate
case (NousResearch#92859).

Add the symmetric IS NULL clause for _branched_from (sibling
_NON_CONTINUATION_CHILD_FILTER_SQL already classifies both markers, and
52a293f recently extended that class to _reset_from). _reset_from stays
inheriting on purpose: a reset fork continues the user's own conversation,
so its routing requirement differs from delegate/branch children.

Tests: the delegate invariant is now parametrized over both markers
(delegate/branch); compression-fork recovery stays green. RED verified on
the previous head (branch param failed with "inherited session_key"),
GREEN with the clause, and a mutation removing only the branch clause is
killed by the branch param.
teknium1 added a commit that referenced this pull request Sep 20, 2026
…its its route

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>
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks @strzhao — this landed on main via #116740 (df2865f), cherry-picked with your authorship preserved: fix(state): delegate and branch children no longer inherit a compressed parent's chat route (#116322, salvage #116377)

Closing this PR as landed/superseded.

@teknium1 teknium1 closed this Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/delegate Subagent delegation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: create_session copies compressed parent routing columns into a marked delegate child

3 participants