Conversation
…ge as user text a48b4c7 (NousResearch#117569) pops _db_persisted when a repair rewrites an already-written dict, on the premise that the next flush rewrites the row. The flush only appends (except blank assistant rows), so the repaired row was INSERTed a second time: - Thinking-only length truncation: the continuation nudge lands right after this turn's written user row; the pre-request repair merges it in and pops the marker, so state.db holds the prompt twice, the second copy carrying "[System: Your previous response was truncated ...]" as the user's own words, replayed on every later turn. - A prompt carried past an interrupted turn-lease wait keeps _persist_after_admission_interrupt forever, so the follow-up turn's merge into it bypasses the history skip and is appended again. - Surrogate recovery rewriting an already-flushed tool result appends a second result for the same tool_call_id. Fix forward, keeping NousResearch#117120's intent (in-place repairs reach state.db): - transcript_repair: a dict whose _row_id owns an active row of its role (or that row's watermark clone) is rewritten in place (content, api_content), never inserted; assistant rows keep the blank-update / concurrent-winner rules. - turn_truncation: a nudge that would follow a user row is request-only scaffolding (_length_continuation_synthetic): never persisted, never merged into the real row, popped before the final answer. The wire copy still merges it, so the request is unchanged. - session_persistence: the admission-interrupt flag is cleared once the carried row is written.
7 tasks
This branch has not been deployed
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?
A repaired user row is saved once, and Hermes's internal length-continuation nudge is no longer saved as the user's own words.
Before this change, #117569 popped a saved row's marker so that "the next flush rewrites it". But the flush only appends, so the user's prompt was inserted a second time and every later turn replayed both. The thinking-only length retry and the interrupted lease-wait carry both hit it.
Related Issue
Fixes #120861
This fixes forward from #117569 (#117120, salvage of #117121) and keeps its intent: an in-place repair of a persisted dict reaches state.db, now as an in-place rewrite. Nothing is reverted.
Type of Change
Changes Made
A.
agent/transcript_repair.py: a message that already owns an active row (_row_id) is never inserted again._active_assistant_rowbecomes_active_row(role), with the same watermark-clone lookup.B.
agent/turn_truncation.py,session_persistence.py,turn_final_response.py: a length nudge that would follow this turn's already-saved user row is flagged_length_continuation_synthetic. That makes it request-only ephemeral scaffolding, like_dropped_toolcall_nudge: the wire copy still merges it for alternation, but it is never saved or merged into the real row, and it's removed before the final answer.test_empty_stub_only_appends_continuation_user_message, which requires the chunking nudge to reach the model.C.
agent/session_persistence.py:_PERSIST_AFTER_ADMISSION_INTERRUPTis cleared after its one write, so the carried row stops bypassing the history skip.agent/agent_runtime_helpers.py_merge_consecutive_usersnever merges ephemeral scaffolding into a real user row.Tests (
tests/agent/test_session_persistence_repaired_user_row.py) drive the real turn loop with a fake LLM client and a real SessionDB. No request leaves the machine.test_thinking_only_length_retry_saves_the_prompt_once_and_never_the_nudgetest_prompt_carried_past_an_interrupted_lease_wait_is_saved_once: a real held lease, the status callback, andinterrupt().main, with the saved prompt duplicated and the nudge saved as user text. Reverting each piece of B and C on its own reddens a test.UnicodeEncodeErrorrecovery, saved one tool row instead of two;'first'plus'first\n\nsecond'.Happy to add either as a third test if wanted.
Trade-off: after such a continuation, the next turn replays the prompt without the nudge, so the prompt cache misses from that message onward, exactly as
_dropped_toolcall_nudgealready does. Before, the corrupted merged text was what got cached.Open PRs:
_length_continuation_syntheticwhen it follows a user row.How to Test
scripts/run_tests.shovertest_session_persistence_repaired_user_row.pyand the fix(agent): in-place repairs of persisted message dicts reach state.db instead of leaving stale rows (#117120, salvage #117121) #117569 suites (test_message_sequence_repair,test_micro_compaction,test_tool_call_args_sanitizer,test_unicode_ascii_codec), plustest_partial_stream_finish_reason: 143 passed._row_id: 1918 passed.test_run_agent.pyand one intui_gatewayfail identically on unmodifiedmainon this host: the optionalanthropicSDK is missing, and a git-probe fork segfaults.[user 'first question', assistant, user 'write me a long report', assistant], with the prompt once and no nudge row.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass. I ran only the related files above throughscripts/run_tests.sh, not the full suite.Documentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A