Skip to content

fix(sessions): recover orphaned compression parents without continuations (#80337) - #80487

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:fix/salvage-80380-orphan-recovery
Aug 7, 2026
Merged

fix(sessions): recover orphaned compression parents without continuations (#80337)#80487
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:fix/salvage-80380-orphan-recovery

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Recovers compression-parent sessions whose continuation was never persisted — the "orphan" state from #80337 where every write raises CompressionSessionClosedError, every turn ends session_persistence_failed, and the only fix was manual SQL on state.db. Salvages @izumi0uu's #80380 (2 commits, authorship preserved) plus one follow-up fixing a reachable fail-open found in adversarial review.

Changes

  • hermes_state.py: SessionDB.reopen_orphaned_compression_session() — reopens a compression-ended parent only when, inside one BEGIN IMMEDIATE write txn: no canonical continuation child exists (ended or live), and no active compression lease exists (expired leases are reclaimed holder-identity-and-all, so a stale compressor's refresh can't resurrect them). Fails closed on any ambiguity. (@izumi0uu)
  • agent/conversation_compression.py: recover_rotated_compression_session() invokes the reopener only after child adoption finds nothing and no live lease remains — recovery happens transparently at turn start. (@izumi0uu)
  • Follow-up (fail-open fix): compression continuations inherit the rotated agent's model_config verbatim, so a delegate subagent's continuation carries _delegate_from=<the delegate's own parent>. The original marker-presence filters misclassified such a real continuation as a delegate child — reopen returned True with a live continuation present, forking the lineage (verified with a live repro). Markers now only disqualify a child when they point at the queried parent, via a shared _NON_CONTINUATION_CHILD_FILTER_SQL fragment used by both find_live_compression_child (adoption) and the reopener — also removing the duplicated-SQL drift risk. Reopen-failure log raised debug→warning (the failure hard-fails the turn moments later).

The three read-only projection walks (get_compression_tip, list_sessions_rich chain, resume walk) keep marker-presence semantics: they can only under-follow (fail closed → resume shows the parent), and the fixed adoption path self-heals at first turn.

Validation

Scenario Before After
True orphan (compression-ended, no child) every write raises, session bricked reopened at turn start, writes proceed
Mid-chain parent P→C(ended)→G(live) reopen refused (fail closed)
Continuation with inherited _delegate_from reopen forked the lineage (pre-follow-up) reopen refused; adoption finds the continuation
Active/expired compression lease active: refused; expired: reclaimed with holder identity, refresh-after loses
  • 3-agent review + adversarial probe batch: TOCTOU (reopen vs publish_compression_child) serializes via BEGIN IMMEDIATE — no window; model_config shape probes all fail closed except the inherited-marker case fixed here; archived-rows/idempotency/lease races all pass
  • Perf: find_live_compression_child +0.005 ms vs before (identical query plan); reopener runs once per recovery attempt, never inside the retry loop
  • Tests: 102 passed across tests/state/, tests/agent/test_compression_orphan_recovery.py, tests/hermes_state/; 69 passed in tests/gateway/test_session.py + tests/agent/test_turn_context.py; regression pair added for the inherited-marker case (reopen fails closed / adoption succeeds)

Credit

Fixes #80337. Supersedes #80380 (salvage), #80399.

izumi0uu and others added 3 commits August 6, 2026 22:22
…open fix)

Adversarial review of the salvaged recovery found a reachable fail-open:
compression continuations 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 parent>. The marker-PRESENCE
filters in reopen_orphaned_compression_session and
find_live_compression_child misclassified such a REAL continuation as a
delegate child:

- reopen: parent 'orphaned' -> reopened while a live continuation exists
  -> two live heads in one lineage (verified with a live repro)
- find_live: adoption misses the continuation (fail-closed, masked the
  fork pre-PR; the PR made it active)

Fix: markers only disqualify a child when they point at the queried
parent (shared _NON_CONTINUATION_CHILD_FILTER_SQL fragment, also
resolving the duplicated-SQL drift risk flagged by the reuse reviewer).
Both directions regression-tested: reopen fails closed on an
inherited-marker continuation; find_live adopts it.

Also from review: reopen-failure log raised debug->warning (the failure
hard-fails the turn moments later), commit-semantics hardening comment
on the lease DELETE path, blank-line nit.

The three read-only projection walks (get_compression_tip,
list_sessions_rich chain, resume walk) share the marker-presence shape
but fail closed (skip a continuation -> resume shows the parent), and
the fixed adoption path self-heals that case at turn start; left as-is.
@alt-glitch alt-glitch added type/bug Something isn't working 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 P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 6, 2026
@ayushnangia

Copy link
Copy Markdown
Contributor

Clean close of the recovery member — the lease-reclaim-inside-one-txn and the marker-target (not presence) disqualification are both the right calls, and factoring _NON_CONTINUATION_CHILD_FILTER_SQL into the shared fragment quietly settles something bigger: find_live_compression_child is now unambiguously the canonical "where did this session go" resolver.

Which leaves two residuals from the class map on #80337 (posted just before this PR):

  1. Readers that still bypass the resolver. The heartbeat poller and ACP resume follow the raw session id — that's fix(gateway): keep /heartbeat alive across a compression session rotation #80225 (@Drexuxux) / fix(gateway): follow session rotation in the heartbeat poller #80208 (@0xGr1mm) and fix(acp): resume compression continuations #80423 (@valo) each teaching one reader about rotation separately. With your shared filter in place, routing those readers through find_live_compression_child is now a small, uniform change. Happy to ship that as a follow-up PR mirroring this one's shape if useful.

  2. The tap. This PR makes orphans survivable but v0.19.1 (2026.7.30) postdates the atomic publish_compression_child, so something is still creating them. Best causal theory so far: _profile_for_child is read from get_active_profile_name() at rotation time — a profile/HERMES_HOME switch mid-rotation (the reporter's log shows a model switch + "stale runtime identity; rebuilding" at exactly that moment) would publish the child transactionally into a different profile's state.db, orphaning the parent in this one. One query decides it against the reporter's machine: SELECT id, started_at FROM sessions WHERE parent_session_id='ms62pbwmvohonq' across every profile's state.db. If it confirms, the fix is publishing through the parent's DB handle rather than the active profile's.

@spfcraze

spfcraze commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The description's supersede accounting doesn't match the repo's current state: #80399 is still open, so merging this PR leaves both #80399 and #80380 open — only #80337 carries a closing keyword.

Problems:

Solution:
If both are intended to be closed by this fix, add closing keywords — "Closes #80380" / "Closes #80399" — or correct the "closed with explanation" sentence to match the actual states.


Checked against 6ec2d26 — the tip of fix/salvage-80380-orphan-recovery when this was written.

@ayushnangia

Copy link
Copy Markdown
Contributor

Closing the loop on the reader-boundary offer above: reviewed the three reader fragments in depth — #80423 already routes ACP resume through the canonical resolver (endorsed there), and the heartbeat member has a duplicate pair (#80225/#80208, adjudication with a compose suggestion posted on #80225). A fourth implementation would be noise, so the offer resolves as coordination instead. The cross-profile source hypothesis on #80337 remains the open tap question.

ayushnangia added a commit to ayushnangia/hermes-agent that referenced this pull request Aug 14, 2026
…ousResearch#80921)

Deterministic, LLM-free conformance cells against the real SessionDB with
real SIGKILL mid-write, per the tracking issue's spot-probe method:

- cell 1: acknowledged-append durability + recovery determinism (adapted
  from the issue's 29.5K probe, scaled kill window, identical assertions)
- cell 2: consume-once under 8-process concurrent claim_handoff
- cell 3 (new): compression-rotation atomicity — never a compression-ended
  parent without a continuation (NousResearch#80337 contract; NousResearch#80487 recovery context)
- cells 4-5: documented stubs interlocked with NousResearch#82956-NousResearch#82959 and
  NousResearch#83197/NousResearch#83557

Journal-mode matrix (resolver default / DELETE / WAL-with-skip-gate) per
cell; every wait deadline-bounded; writers asserted alive at kill time.
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 P1 High — major feature broken, no workaround 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.

[Bug]: Compression-closed session with no continuation becomes an orphan that rejects all writes — turns die with session_persistence_failed

5 participants