Skip to content

fix(session-state): lineage walks survive cleared end_reason on reopened links - #92533

Open
sebmarion wants to merge 1 commit into
NousResearch:mainfrom
sebmarion:fix/archive-lineage-reopened-links
Open

fix(session-state): lineage walks survive cleared end_reason on reopened links#92533
sebmarion wants to merge 1 commit into
NousResearch:mainfrom
sebmarion:fix/archive-lineage-reopened-links

Conversation

@sebmarion

Copy link
Copy Markdown

PR: fix(session-state): lineage walks survive cleared end_reason on reopened links

Open with:
https://github.com/NousResearch/hermes-agent/compare/main...sebmarion:hermes-agent:fix/archive-lineage-reopened-links?expand=1

Problem

Archiving a conversation only archived part of it, and the chat kept resurfacing in the sidebar. Reproduced in production.

Root cause chain:

  1. The TUI websocket reaper closes stale sessions with end_reason = 'ws_orphan_reap'.
  2. Stale-route recovery later calls reopen_session, which clears ended_at/end_reason (by design — that is how resume works).
  3. Every lineage walk (set_session_archived, set_session_pinned, set_session_hidden, set_session_read) traverses only parent.end_reason = 'compression' edges.
  4. Result: the compression link through a reopened row is invisible. Archive covers only the segment below the break; the unarchived, re-active root keeps the conversation alive in the sidebar, and new continuation rows are born unarchived.

The codebase already acknowledges this fragility: reopen_session stamps children with $._reset_from before clearing the parent's mutable end_reason, precisely because other logic depends on it. Compression chains had no equivalent durable marker.

Fix

Follows the existing _reset_from pattern:

  1. Stamp at rotation: publish_compression_child writes $._compression_from = <parent id> into the continuation child's model_config (idempotent, NULL-guarded).
  2. Stamp at creation: _insert_session_row stamps children created under an already-ended compression parent — covers the documented race where a child row is inserted before the parent's ended_at lands.
  3. Walk with fallback: each of the four lineage-flipping CTEs gains one OR-arm that follows the raw parent edge when either side carries the marker.

get_compression_tip is intentionally untouched: once archive/pin cover the whole lineage, the archived filter hides the root regardless of tip projection; resume-ordering semantics are unchanged. Diff stays minimal (64 lines in hermes_state.py, all additive).

Testing

  • New regression tests in tests/hermes_state/test_session_archiving.py: real-rotation stamping, reap→reopen→archive reaching the whole lineage, unarchive restoring it, pin coverage, and creation-time stamping under an already-ended parent.
  • tests/hermes_state/ — 156 passed
  • tests/agent/test_compression_rotation_state.py tests/agent/test_compression_concurrent_fork.py tests/run_agent/test_compression_boundary_hook.py — 70 passed

…ned links

A mistaken TUI reaper close ('ws_orphan_reap') followed by stale-route
recovery (reopen_session) clears the parent's mutable end_reason. Every
lineage walk keyed on end_reason='compression' then silently lost the
link: set_session_archived / set_session_pinned / set_session_hidden /
set_session_read only covered the segment below the break, so the
unarchived, re-active root kept resurrecting the conversation in the
sidebar after the user archived it.

Fix follows the existing _reset_from pattern (reopen_session already
stamps children before clearing mutable end_reason):

1. publish_compression_child stamps $._compression_from=<parent> on the
   continuation child at rotation time.
2. _insert_session_row stamps children created under an already-ended
   compression parent (covers the child-inserted-before-ended_at race).
3. The four lineage-flipping CTEs gain a fallback arm following the raw
   parent edge when the marker is present.

get_compression_tip is intentionally untouched: with archive covering
the whole lineage, the archived filter hides the root regardless of tip
projection, and resume-ordering semantics stay unchanged.
@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 22, 2026
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.

2 participants