fix(agent): invalidate flush-scan cursor when finalizer pops db marker - #75170
Closed
spfcraze wants to merge 1 commit into
Closed
fix(agent): invalidate flush-scan cursor when finalizer pops db marker#75170spfcraze wants to merge 1 commit into
spfcraze wants to merge 1 commit into
Conversation
The bounded flush-scan in _flush_messages_to_session_db_unlocked skips the identity-matched prefix of its previous snapshot, on the documented assumption that no code path pops _DB_PERSISTED_MARKER from a live dict in place. finalize_turn's pure-tool-call-tail fill is exactly that path: it pops the marker so the filled content gets re-persisted — but the cursor then skips the row anyway, so the delivered final response never reaches state.db and /resume replays content="" (the NousResearch#43849/NousResearch#44100 class resurfacing via the perf cursor). Invalidate the cursor at the pop site so the filled row is re-examined.
teknium1
added a commit
that referenced
this pull request
Aug 1, 2026
…ling site The micro-compaction defrag pass (_defrag_rolling_summary) rewrites the newest MICRO marker's content and pops _DB_PERSISTED_MARKER from the LIVE dict in place — the same in-place pop class finalize_turn's fill site was fixed for in #75170. Without invalidation the bounded flush-scan cursor identity-skips the rewritten marker row and the defragged rolling summary never reaches state.db (resume rehydrates a stale summary). The compressor holds no agent reference, so the pop site raises _flush_scan_cursor_invalidated and the finalize_turn micro-compaction block consumes it, setting agent._db_flush_scan_prefix = None. The module-scope pop sites (context_compressor.py:175/224) operate on fresh copies — identity-breaking by construction — and need no flag. Follow-up to #75170 (fix-the-class sweep of _DB_PERSISTED_MARKER in-place pops).
teknium1
added a commit
that referenced
this pull request
Aug 1, 2026
…ling site The micro-compaction defrag pass (_defrag_rolling_summary) rewrites the newest MICRO marker's content and pops _DB_PERSISTED_MARKER from the LIVE dict in place — the same in-place pop class finalize_turn's fill site was fixed for in #75170. Without invalidation the bounded flush-scan cursor identity-skips the rewritten marker row and the defragged rolling summary never reaches state.db (resume rehydrates a stale summary). The compressor holds no agent reference, so the pop site raises _flush_scan_cursor_invalidated and the finalize_turn micro-compaction block consumes it, setting agent._db_flush_scan_prefix = None. The module-scope pop sites (context_compressor.py:175/224) operate on fresh copies — identity-breaking by construction — and need no flag. Follow-up to #75170 (fix-the-class sweep of _DB_PERSISTED_MARKER in-place pops).
teknium1
added a commit
that referenced
this pull request
Aug 1, 2026
…ling site The micro-compaction defrag pass (_defrag_rolling_summary) rewrites the newest MICRO marker's content and pops _DB_PERSISTED_MARKER from the LIVE dict in place — the same in-place pop class finalize_turn's fill site was fixed for in #75170. Without invalidation the bounded flush-scan cursor identity-skips the rewritten marker row and the defragged rolling summary never reaches state.db (resume rehydrates a stale summary). The compressor holds no agent reference, so the pop site raises _flush_scan_cursor_invalidated and the finalize_turn micro-compaction block consumes it, setting agent._db_flush_scan_prefix = None. The module-scope pop sites (context_compressor.py:175/224) operate on fresh copies — identity-breaking by construction — and need no flag. Follow-up to #75170 (fix-the-class sweep of _DB_PERSISTED_MARKER in-place pops).
Contributor
|
Merged via salvage PR #75883 (#75883) — your commit cherry-picked with authorship preserved, plus a follow-up applying the same cursor invalidation at the sibling in-place marker-pop site in context_compressor's defrag path (fix-the-class). The sabotage-verified test methodology in your PR made verification easy. Thanks! |
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…ling site The micro-compaction defrag pass (_defrag_rolling_summary) rewrites the newest MICRO marker's content and pops _DB_PERSISTED_MARKER from the LIVE dict in place — the same in-place pop class finalize_turn's fill site was fixed for in NousResearch#75170. Without invalidation the bounded flush-scan cursor identity-skips the rewritten marker row and the defragged rolling summary never reaches state.db (resume rehydrates a stale summary). The compressor holds no agent reference, so the pop site raises _flush_scan_cursor_invalidated and the finalize_turn micro-compaction block consumes it, setting agent._db_flush_scan_prefix = None. The module-scope pop sites (context_compressor.py:175/224) operate on fresh copies — identity-breaking by construction — and need no flag. Follow-up to NousResearch#75170 (fix-the-class sweep of _DB_PERSISTED_MARKER in-place pops).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes the delivered final response silently going missing from the durable transcript when a turn ends on a pure tool-call tail.
Two mechanisms collide:
finalize_turn's tail fill (agent/turn_finalizer.py): when the transcript tail is a pure tool-call assistant row with empty content but afinal_responsewas delivered, it fills that row'scontentin place and pops_db_persisted— explicitly so the next_persist_sessionre-writes the filled content (the fix for Gateway can deliver assistant response without persisting assistant row #43849/Telegram: assistant responses not persisted to session DB (model re-answers old messages) #44100).run_agent.py:_flush_messages_to_session_db_unlocked, perf commit30c783589): skips the identity-matched prefix of the previous flush's snapshot, on the documented assumption that "no code path pops_DB_PERSISTED_MARKERfrom a live dict in place."That assumption is false — the tail fill is the one place that does. The filled row is the same dict object flushed mid-turn, so the identity match skips it and the marker pop has no effect: the user sees the answer, but
state.dbkeepscontent=""and/resumereplays an unanswered backlog, so the model re-answers — the exact #43849/#44100 symptom, resurfacing via the perf cursor.The fix invalidates the cursor at the pop site (
agent._db_flush_scan_prefix = None), so the filled row is re-examined. The perf optimization is untouched everywhere else; the append-only flush design is preserved (the re-persist appends the filled row, same as pre-cursor behavior).Related Issue
No GitHub issue — discovered via code review and reproduced live (see below). Happy to file one first if preferred.
Type of Change
Changes Made
agent/turn_finalizer.py: after popping_db_persistedin the tail fill, invalidateagent._db_flush_scan_prefix(with a comment explaining the interaction).tests/agent/test_turn_finalizer_final_response_persistence.py: new test — the fill path invalidates the cursor (and pops the marker) on a pure tool-call tail.tests/run_agent/test_identity_flush.py: new end-to-end test — incremental flush stamps the tool-call row; after fill + pop + invalidation, the turn-end flush persists the filled answer to a realSessionDB.How to Test
Reproduction (against pre-fix code, real
AIAgent+ realSessionDB):Focused validation completed:
bash scripts/run_tests.sh tests/agent/test_turn_finalizer_final_response_persistence.py tests/run_agent/test_identity_flush.py— 9/9 pass.agent/turn_finalizer.pychange makes exactly the cursor-invalidation test fail (8 pass); restoring returns to 9/9.tests/agent/test_turn_finalizer_*+ the fulltests/run_agent/directory — 1278/1278 pass.bash scripts/run_tests.sh(branch): 22,817 pass / 100 fail. Baseline on cleanmain(this branch's parent), same machine: 22,816 pass / 99 fail — failing sets identical excepttests/honcho_plugin/test_pin_peer_name.py, which fails identically standalone on cleanmain(pre-existing flake, no agent-persistence imports). Nothing in the persistence/finalizer surface fails.uvx --from ruff==0.15.10 ruff check agent/turn_finalizer.py tests/agent/test_turn_finalizer_final_response_persistence.py tests/run_agent/test_identity_flush.py— clean.git diff --check— clean.test_final_response_fills_pure_tool_call_tailstill passes unchanged.The full repo-wide suite was run locally (item 4) with results verified against clean
main; GitHub CI remains the final confirmation environment.Checklist
Code
fix(scope):,feat(scope):, etc.)run_agent.py/agent/turn_finalizer.py— closest were the in-flight-marker family fix(agent): preserve in-flight marker on flush failure #74305/fix(state): don't clear in-flight turn marker when canonical flush fails #74540 and fix(agent): do not re-persist empty failure sentinel after scaffolding drop #69279's sentinel drop, all different defects)scripts/run_tests.sh): 22,817 pass / 100 fail — failures verified identical to cleanmain(environment lanes; not all pass, so the stock claim above is restated accurately here — see How to Test, item 4)Documentation & Housekeeping
docs/, docstrings) — or N/A (inline comment documents the cursor/marker interaction)cli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/ALogs
Sabotage verification output: