Skip to content

fix(state): dedup list_sessions_rich's recursive chain CTE (UNION not UNION ALL) - #61201

Open
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/list-sessions-rich-chain-cte-cycle-hang
Open

fix(state): dedup list_sessions_rich's recursive chain CTE (UNION not UNION ALL)#61201
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/list-sessions-rich-chain-cte-cycle-hang

Conversation

@pierrenode

Copy link
Copy Markdown
Contributor

Summary

list_sessions_rich's order_by_last_active=True path builds a recursive chain CTE that walks compression-continuation edges forward via child.parent_session_id = cur_id, using UNION ALL. A corrupted parent chain that loops (a -> b -> a) never terminates: UNION ALL keeps re-deriving the same (root_id, cur_id) pairs forever since it doesn't dedup the working set.

This is the same class of bug just fixed in the sibling _session_latest_descendant query (1e2ad17, "#39140 CTE ... recurses forever if a corrupted parent chain loops") — a separate, pre-existing recursive CTE in this file that fix didn't touch. list_sessions_rich is used far more broadly (dashboard, CLI, TUI, ACP adapter, ~20 call sites), so a corrupted/cyclic compression chain reaching this path would hang whichever caller invoked it, not just one endpoint.

Fix

UNION instead of UNION ALL. A legitimate acyclic chain never produces a duplicate (root_id, cur_id) pair (parent_session_id is single-valued per row), so this is a no-op for correct data and only changes behavior for the corrupted-cycle case.

Tests

tests/test_hermes_state.py — builds a 2-session cycle (cyc-a <-> cyc-b, both end_reason='compression', mutually pointing at each other via parent_session_id) and asserts list_sessions_rich(..., order_by_last_active=True, include_children=True) returns instead of hanging.

Test plan

  • Mutation-verified empirically, not just by code inspection: reverting the fix and running the new test with a bounded 8s timeout confirms it actually hangs (times out) against the pre-fix code; a raw-SQL reproduction against the exact cyclic fixture data independently confirmed the same UNION ALL query never returns. With the fix, the same test passes in under 0.1s.
  • Full hermes_state.py suite (340 tests) passes
  • Session-listing/filters/archiving suites (166 tests) pass
  • hermes_cli/web_server.py full suite (366 tests) passes
  • ruff check clean on both changed files

… UNION ALL)

list_sessions_rich's order_by_last_active=True path builds a recursive
"chain" CTE that walks compression-continuation edges forward via
child.parent_session_id = cur_id, using UNION ALL. A corrupted parent
chain that loops (a -> b -> a) never terminates: UNION ALL keeps
re-deriving the same (root_id, cur_id) pairs forever since it doesn't
dedup the working set.

This is the same class of bug just fixed in the sibling
_session_latest_descendant query (1e2ad17, "NousResearch#39140 CTE ... recurses
forever if a corrupted parent chain loops") — a separate, pre-existing
recursive CTE in this file that fix didn't touch. list_sessions_rich is
used far more broadly (dashboard, CLI, TUI, ACP adapter, ~20 call
sites), so a corrupted/cyclic compression chain reaching this path
would hang whichever caller invoked it, not just one endpoint.

Fix: UNION instead of UNION ALL. A legitimate acyclic chain never
produces a duplicate (root_id, cur_id) pair (parent_session_id is
single-valued per row), so this is a no-op for correct data and only
changes behavior for the corrupted-cycle case.

Tests: tests/test_hermes_state.py -- builds a 2-session cycle
(cyc-a <-> cyc-b, both end_reason='compression', mutually pointing at
each other via parent_session_id) and asserts list_sessions_rich(...,
order_by_last_active=True, include_children=True) returns instead of
hanging.

Mutation-verified empirically, not just by code inspection: reverting
the fix and running the new test with a bounded 8s timeout confirms it
actually hangs (times out) against the pre-fix code; a raw-SQL
reproduction against the exact cyclic fixture data independently
confirmed the same UNION ALL query never returns. With the fix, the
same test passes in under 0.1s. Full hermes_state.py (340),
session-listing/filters/archiving (166), and web_server.py (366) suites
pass.
@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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 9, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Looks Good

  • Fixes duplicate results in list_sessions_rich by changing UNION to UNION ALL in the recursive CTE
  • Scoped SQLite query fix, no side effects
  • No security concerns

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused regression fix. Current main still has the recursive UNION ALL at hermes_state.py:3172-3182; the PR's UNION change prevents a revisited (root_id, cur_id) pair from re-entering the recursive working set. The added cyclic-fixture test directly reaches the affected order_by_last_active=True path.

Sibling recursive session traversals already use UNION in hermes_state.py:2691-2700, hermes_state.py:2777-2799, and hermes_cli/web_server.py:9395-9402, so the patch addresses the remaining matching recursive CTE without expanding scope.

Automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit label Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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.

4 participants