Skip to content

fix(compression): adopt live continuation tip at flush across multi-hop chains - #86666

Merged
teknium1 merged 1 commit into
mainfrom
fix-flush-adopt-compression-tip
Aug 15, 2026
Merged

fix(compression): adopt live continuation tip at flush across multi-hop chains#86666
teknium1 merged 1 commit into
mainfrom
fix-flush-adopt-compression-tip

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

A turn writing against a session already closed by context compression died with session_persistence_failed and the misleading "this is often a full disk" dialog — even though the store was healthy and a live continuation existed. Fixes #82001.

The depth-1 recovery API (find_live_compression_child) could not resolve lineages with ≥2 compression hops (root(compressed) → mid(compressed) → tip(live)): the direct child is itself compression-ended, so the lookup returned nothing, adoption failed closed, and the turn died. Reproduced independently three times on this issue (two-hop production trace, minimal root → mid → tip probes, and a three-hop deep-research workload).

Changes

  • Agent flush (run_agent.py _flush_messages_to_session_db_unlocked): on CompressionSessionClosedError, resolve tip = db.get_compression_tip(old_id) — the canonical bounded transitive walk (excludes _branched_from/_delegate_from/source='tool' children, prefers live over stale closed siblings like ws_orphan_reap). Adopt only when tip != old_id and the tip row is live (ended_at IS NULL), then retry the flush exactly once (adoption budget) — a second closed-parent write fails closed, no loop. get_compression_tip returns the input id when no continuation exists, so tip == old_id is treated as no-adopt/fail-closed.
  • Gateway transcript flush (gateway/session.py append_to_transcript): the reroute now uses the same tip + liveness contract instead of the depth-1 lookup, so multi-hop lineages resolve on the gateway write side too.
  • Turn-start recovery preflight (agent/conversation_compression.py _adopt_live_compression_child): the last depth-1 consumer in this family. It resolved a unique live direct child, loaded its transcript, and revalidated before mutating the agent — semantics that map cleanly onto tip resolution, so it now resolves via get_compression_tip with the same liveness check and post-load revalidation (confirmed tip == adopted tip). Fail-closed behavior for no-continuation / closed-tip lineages is preserved; recover_rotated_compression_session's orphan-reopen fallback is untouched.
  • Diagnostics: classify_persistence_error gains a compression_closed bucket (added to PERSISTENCE_ERROR_CAUSES; matched by exception type and by the "closed by compression" phrase for RPC-wrapped strings). The turn-completion explanation for this cause names compression rotation and tells the client to refresh/pick up the new session id — it never mentions disk. Real disk failures keep the disk guidance. The per-turn _compression_adoption_failed diagnostic flag is reset at turn start.

Fail-closed semantics are strictly preserved: no live tip → no adoption, no message content or ordering is touched (this is session-identity plumbing only), and the adopted retry replays the exact same unpersisted batch.

Tests

tests/run_agent/test_compression_closed_adoption.py (new, real temp SQLite via hermes_state, no live model):

  • depth-1 unique live child adoption at flush (message lands in the child; closed parent stays immutable)
  • multi-hop chain root(compressed) → mid(compressed) → tip(live) adopts the tip — the regression the depth-1 API missed
  • no continuation → fail closed, flush returns False, cause = compression_closed
  • stale-closed tip (ws_orphan_reap) → fail closed, never adopt a closed session
  • adoption budget: tip lookup runs exactly once; a second closed write after adoption fails closed (no loop)
  • wording guards: compression-closed explanation never contains "disk"; the disk cause keeps "full disk" guidance

tests/gateway/test_session.py: multi-hop chain reroute to the live tip, fail-closed on a stale-closed tip, and the existing backlog-migration mock moved to the canonical get_compression_tip API.

tests/run_agent/test_compression_closed_adoption.py ........ 8 passed
tests/gateway/test_session.py (TestGatewaySessionDbRecovery) 6 passed
Regression: tests/gateway/test_session.py, tests/run_agent/test_turn_completion_explainer.py,
tests/state/test_compression_lineage_guard.py, tests/agent/test_compression_* ,
tests/run_agent/test_cross_process_turn_lease.py, tests/test_hermes_state.py,
tests/cron/test_scheduler.py, tests/run_agent/test_413_compression.py,
tests/run_agent/test_860_dedup.py, test_81641 → 539 passed, 1 pre-existing
failure (tests/test_hermes_state.py FTS5 projection test, fails on clean origin/main too)

Relationship to existing work

Infographic

Flush adopts live compression tip

…op chains

A turn writing against a session already closed by compression died with
session_persistence_failed and a misleading "this is often a full disk"
dialog, even though the store was healthy and a live continuation existed
(#82001). Depth-1 recovery (find_live_compression_child) could not resolve
lineages with >=2 compression hops (root -> mid -> tip), reproduced
independently on two- and three-hop chains.

- run_agent.py flush chokepoint: on CompressionSessionClosedError, resolve
  tip = db.get_compression_tip(old_id) (canonical bounded transitive walk),
  adopt only when tip != old_id AND the tip row is live, retry the flush
  exactly once (adoption budget); otherwise fail closed.
- gateway/session.py append_to_transcript: replace the depth-1 live-child
  lookup with the same tip + liveness contract, so gateway transcript
  reroutes follow full chains.
- agent/conversation_compression.py _adopt_live_compression_child: turn-start
  recovery preflight now resolves via get_compression_tip with the same
  liveness check, closing the last depth-1 consumer in this family.
- classify_persistence_error: new "compression_closed" bucket; the turn-end
  explanation names compression rotation and tells the client to refresh the
  session id instead of blaming a full disk.

Tests: depth-1 adoption, multi-hop chain adoption (agent + gateway), fail
closed with no continuation / stale-closed (ws_orphan_reap) tip, exactly-once
adoption budget, and error-wording guards (compression-closed never mentions
disk; real disk failures keep disk guidance).

Closes #82001

Co-authored-by: Al3xand3r1987 <125030427+Al3xand3r1987@users.noreply.github.com>
Co-authored-by: yuzilongleif-collab <235949691+yuzilongleif-collab@users.noreply.github.com>
@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 comp/gateway Gateway runner, session dispatch, delivery area/compression Context compression and continuation sessions area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state needs-decision Awaiting maintainer decision before any implementation labels Aug 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on 3d6cedb — fix(compression): adopt live continuation tip at flush acros


waiting for more jobs to start…

⚠️ Warnings

OSV vulnerability scan · View job

5 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

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 comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation 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.

[Bug]: Agent flush does not adopt live continuation after compression — turns die with misleading "full disk" dialog

2 participants