fix(agent): adopt live compression continuation when flushing to a closed session - #86409
fix(agent): adopt live compression continuation when flushing to a closed session#86409kshitijk4poor wants to merge 3 commits into
Conversation
…osed session When context compression closes a session while a client still writes against the old id (WebUI/streaming clients resend the pre-compression session id every turn), the transcript flush swallowed CompressionSessionClosedError as a generic write failure: the turn died with session_persistence_failed and the user was told 'this is often a full disk' even though the DB was healthy. The stale id reproduced the failure on every follow-up turn until the client reloaded. The flush chokepoint now catches CompressionSessionClosedError, resolves the unique live continuation via find_live_compression_child (at most one probe per flush), rebinds session_id, and replays the failed batch once. Zero or multiple children — and a second Closed error on the adopted child — still fail closed, but classify_persistence_error now maps the closed-parent error to the 'compression' cause so the turn-end explanation names compression rotation instead of disk advice. Fixes NousResearch#82001 Reported-by: @Al3xand3r1987
|
Quick data point from our side — no action needed for this PR. We've been running the transitive variant ( Not a criticism of this PR — the minimal fail-closed fix with honest copy is the right scope. This follow-up applies regardless of whether #86409 or #79763 ultimately lands: if multi-hop turns out to be common, promoting the resolver to the transitive tip would cover those chain cases too. I can share the concrete 3-hop regression test and repro if useful. |
…id writers Review follow-up on the NousResearch#82001 fix: - Extract rebind_agent_compression_side_state() from _adopt_live_compression_child() and use it in the flush-time adoption, so both adoption paths move the session ContextVar/env, logging context, _flushed_db_message_session_id guard, and compressor/ memory-manager boundary notifications together instead of the flush path rebinding only session_id. Load-specific state stays in the turn-start path, which is the only one that reloads messages. - Dedupe the flush's two verbatim append_messages_batch calls into a local _append_batch(sid) closure so the retry can't drift from the primary call. - Fix two sibling writers in the same bug class: - tui_gateway/server.py model-switch marker persisted to the stale session_key after rotation (silently dropped via debug-logged except); use agent.session_id like the NousResearch#20001 fix established. - gateway/shutdown_flush.recover_pending_to_db replayed spooled rows to the recorded id; a compression-closed id raised on every startup and retried the same file forever. New _append_with_compression_adoption() probes find_live_compression_child once and preserves the file on 0/>1 children. - Tests: 3 new spool-recovery/helper tests; turn-start adoption suites (233 selected) all green against the shared helper.
…on id Updates the assertion pinned to the old stale-key behavior changed in the previous commit (session_key -> agent.session_id, the NousResearch#20001/NousResearch#82001 bug class).
|
Verified against the #82001 contract on your branch: the adoption goes through |
fix(agent): adopt live compression continuation when flushing to a closed session Good fix: extracting
|
Summary
An agent turn that flushes its transcript against a session id that compression already closed now adopts the unique live continuation and persists there, instead of dying with
session_persistence_failedand misleading "this is often a full disk" advice.Root cause (#82001): WebUI/streaming clients resend the pre-compression session id every turn.
SessionDB.append_messages_batchcorrectly fails closed withCompressionSessionClosedError, but the flush chokepoint inrun_agent.pyswallowed it as a generic write failure — so every follow-up turn died until the client reloaded, and the error copy blamed the disk.Changes
run_agent.py_flush_messages_to_session_db_unlocked: catchCompressionSessionClosedError, resolve the unique live continuation via the existingfind_live_compression_child()(at most one probe per flush), rebindsession_id, replay the failed batch once. 0 children (orphan window), >1 children (ambiguous lineage), or a second Closed error on the adopted child all fail closed exactly as before.hermes_state.pyclassify_persistence_error: mapCompressionSessionClosedError(by type and by RPC-wrapped string) to thecompressioncause, so the fail-closed path's turn-end explanation names compression rotation instead of disk advice. The existingcompressionwording already fits ("send it again after compression completes").tests/run_agent/test_flush_compression_adoption.py: 7 tests — adopt-unique-child, fail-closed no-child, fail-closed ambiguous, adoption budget bounded to one probe (child closed mid-replay), next-flush-writes-directly-to-child, normal-flush untouched, cause classification.Validation
False, causeunknown, rows lost from turnTrue, rows on live childFalse, "full disk" adviceFalse, honest compression wordingTargeted suites:
tests/run_agent/persistence/flush/compression selection 189 passed;tests/state/test_compression_lineage_guard.py16 passed; new file 7 passed.Scope note: this is part 1 from the issue (agent-side adoption). Part 2 (returning the adopted id through the SSE stream so the WebUI session index converges) is client-facing and remains a separate change, as the issue itself proposes.
Fixes #82001
Credit
Diagnosis, seam taxonomy, and the proposed fix design are from @Al3xand3r1987's exceptionally thorough report in #82001. PR #86099 attempted this area but was closed (see review there); this implements the minimal agent-side fix the issue describes.