fix(agent): invalidate flush-scan cursor when finalizer pops db marker - #10
Open
spfcraze wants to merge 1 commit into
Open
fix(agent): invalidate flush-scan cursor when finalizer pops db marker#10spfcraze 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.
spfcraze
pushed a commit
that referenced
this pull request
Aug 10, 2026
…lves A session title had no notion of who set it, so two bugs followed. An auto-generated title could clobber a name the user typed, and every compression rotation renumbered the conversation it forked - one piece of work reaching 'Smallville Map Architecture Plan #10' in the sidebar. Titles now carry a source (derived < llm < user) enforced by one compare-and-swap, so an automatic write can only ever replace a title of strictly lower authority. Compression carries the name across unchanged. Legacy NULL rows rank as user, so auto-titling only fills genuinely empty titles on existing data.
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 NousResearch/hermes-agent#43849/Telegram: assistant responses not persisted to session DB (model re-answers old messages) NousResearch/hermes-agent#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 NousResearch#43849/NousResearch#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 NousResearch/hermes-agent#74305/fix(state): don't clear in-flight turn marker when canonical flush fails NousResearch/hermes-agent#74540 and fix(agent): do not re-persist empty failure sentinel after scaffolding drop NousResearch/hermes-agent#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: