Skip to content

fix(state): bind record_gateway_session_peer's ancestor walk to the queried parent - #80982

Open
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/gateway-session-peer-compression-lineage-marker-binding
Open

fix(state): bind record_gateway_session_peer's ancestor walk to the queried parent#80982
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/gateway-session-peer-compression-lineage-marker-binding

Conversation

@pierrenode

Copy link
Copy Markdown
Contributor

Summary

Today's a0801b878 fixed a marker-PRESENCE-vs-marker-BINDING bug in find_live_compression_child / reopen_orphaned_compression_session: a real compression continuation can inherit the rotated agent's model_config verbatim (publish_compression_child callers pass agent._session_init_model_config), so a delegate subagent's continuation carries _delegate_from=<the delegate's own original parent> — a marker that exists but does not point at the parent being evaluated. Matching on presence alone misclassified such a continuation as a delegate/branch child.

record_gateway_session_peer(include_compression_ancestors=True)'s recursive CTE (used by gateway/session.py::switch_session on an explicit /resume to keep a compression lineage on one routing peer) has the exact same marker-presence check and was not touched by that fix — it wasn't in the list of read-only sites a0801b878 deliberately left alone (those fail closed and self-heal at next turn start; this is a write path with no such self-heal).

Effect: resuming into a live continuation whose compression parent carries a foreign delegate/branch marker stops the lineage walk one hop too early — the parent keeps its old session_key while the continuation gets the new one, splitting the lineage's routing peer across two keys.

Fix

Same pattern as a0801b878 (COALESCE(json_extract(...), '') != <id> instead of IS NULL), but bound to the recursive CTE's correlated parent.id column rather than a single top-level bound parameter — this walk evaluates a different parent at every hop (unlike the two functions a0801b878 fixed, which query one fixed parent/session id), so the exclusion has to be re-evaluated per row rather than bound once.

Testing

  • Added test_record_gateway_session_peer_walks_multi_hop_compression_lineage (control: plain two-hop chain, no foreign markers) and test_record_gateway_session_peer_walks_past_continuation_with_foreign_marker (repro) to tests/state/test_compression_lineage_guard.py.
  • Mutation-verified: reverted the hermes_state.py change and confirmed the foreign-marker test fails on pre-fix code (AssertionError: assert None == 'new-routing-key').
  • tests/state/, tests/gateway/test_session.py, and the full tests/test_hermes_state.py (187 tests) all pass.
  • ruff check clean on both changed files.

Competing PRs

Searched record_gateway_session_peer, compression lineage marker, gateway session peer compression ancestors, switch_session compression ancestors — no PR targets this function. #71486 ("recover live tip across rotation chains") touches hermes_state.py and the same test file but a different function (find_latest_gateway_session_for_peer, ~250 lines away) with no overlap.

@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 area/sessions Session lifecycle, resume, persistence, history area/compression Context compression and continuation sessions sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
@spfcraze

spfcraze commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The marker-PRESENCE check this PR replaces in record_gateway_session_peer's ancestor walk still gates the resume read path — get_compression_tip (hermes_state.py:6057) and resolve_resume_session_id (hermes_state.py:7552) still exclude a foreign-marker continuation, so resume lands on the parent.

Problems:

  • hermes_state.py:6057 (get_compression_tip) and hermes_state.py:7552 (resolve_resume_session_id) still filter children with json_extract(...) IS NULL on the _branched_from/_delegate_from markers; this PR's diff does not touch either walk.
  • A foreign-marker continuation — the case test_record_gateway_session_peer_walks_past_continuation_with_foreign_marker reproduces — is excluded by both walks, so a resume of the compressed parent returns the parent while the post-compression turns sit in the excluded continuation, the outcome get_compression_tip's docstring describes as the user's latest messages looking "lost".

Solution:
Apply the same binding exclusion (COALESCE(json_extract(...), '') != parent.id, already shared as _NON_CONTINUATION_CHILD_FILTER_SQL) to the child filters in get_compression_tip and resolve_resume_session_id, mirroring the change this PR makes to record_gateway_session_peer.


Checked against 9a7f0b5 — the tip of fix/gateway-session-peer-compression-lineage-marker-binding when this was written — and f15a38e, main at the same moment.

record_gateway_session_peer()'s recursive ancestor walk was already fixed to
bind _branched_from/_delegate_from marker checks to the specific parent
being evaluated, since a compression continuation inherits the rotated
agent's model_config verbatim and can carry a marker pointing at an
unrelated ancestor. get_compression_tip() and resolve_resume_session_id()
shared the same recursive-lineage shape but still used marker-presence-only
(IS NULL) checks, so a continuation with a foreign marker was misclassified
as a non-continuation child — get_compression_tip stopped one hop early and
returned the dead parent, and resolve_resume_session_id's message-bearing
walk skipped the continuation entirely, so --resume on the parent reloaded
the pre-compression transcript missing every post-compression turn.

Both functions now reuse the shared _NON_CONTINUATION_CHILD_FILTER_SQL
filter (already used by find_live_compression_child), binding the marker
check to the specific parent id being walked at each hop instead of
checking marker absence.
@pierrenode
pierrenode force-pushed the fix/gateway-session-peer-compression-lineage-marker-binding branch from 9a7f0b5 to 9a547de Compare August 11, 2026 13:45
@pierrenode

Copy link
Copy Markdown
Contributor Author

Rebased onto current `upstream/main` and extended the fix to the two other functions that share the same recursive-lineage shape as `record_gateway_session_peer()`:

  • `get_compression_tip()` — the forward tip-walk still used marker-presence-only (`IS NULL`) checks. A continuation inheriting a foreign `_delegate_from`/`_branched_from` (pointing at some other ancestor, not the parent being walked) was misclassified as a non-continuation child, so the walk stopped one hop early and returned the dead parent instead of the live tip.
  • `resolve_resume_session_id()` — same bug in its message-bearing-descendant walk. The same foreign-marker continuation was skipped, so `--resume` on the parent reloaded the pre-compression transcript with every post-compression turn missing.

Both now reuse the shared `_NON_CONTINUATION_CHILD_FILTER_SQL` filter (already used by `find_live_compression_child`), binding the marker check to the specific parent id at each hop instead of checking marker absence — same pattern as the original fix to `record_gateway_session_peer()`.

Added two regression tests (`test_get_compression_tip_walks_past_continuation_with_foreign_marker`, `test_resolve_resume_session_id_walks_past_continuation_with_foreign_marker`) mirroring the existing foreign-marker tests in this file. Mutation-verified: both fail against pre-fix code. Full `tests/state/` suite (19/19) plus 560 tests across the broader session/resume/compression neighborhood pass. Ruff clean.

Fresh competitor search: no open PR touches `get_compression_tip`/`resolve_resume_session_id`. #71486 rewrites `find_live_compression_child` only (already correctly fixed upstream); #64620 touches the same bug class but in `hermes_cli/web_server.py`'s separate dashboard query, not `hermes_state.py`.

Squashed to a single commit on top of current `upstream/main`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions 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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants