fix(gateway): atomic JSONL transcript rewrite - #15085
Closed
simbam99 wants to merge 1 commit into
Closed
Conversation
SessionStore.rewrite_transcript opened the transcript with mode="w" and wrote JSON lines directly to the target path. A crash between truncation and final flush left the transcript empty or partial, losing conversation history for sessions whose SQLite DB layer is absent (pre-DB sessions) and corrupting /retry, /undo, /compress, and /reset flows during unclean shutdowns. Add utils.atomic_jsonl_write — a tempfile + fsync + os.replace helper mirroring atomic_json_write / atomic_yaml_write (including _preserve_file_mode / _restore_file_mode for Docker/NAS parity) — and route rewrite_transcript through it so the target file is never observable in an intermediate state. The existing read-side fallback for corrupt JSONL lines (TestLoadTranscriptCorruptLines, NousResearchGH-1193) already confirmed this failure mode; this closes the write-side hole. Tests: - tests/hermes_cli/test_atomic_jsonl_write.py covers the new helper (14 cases: crash-safety, cleanup, unicode, generators, dump kwargs) - tests/gateway/test_session.py adds TestSessionStoreRewriteTranscriptAtomicity covering crash preservation and tempfile cleanup for the transcript rewrite path
Collaborator
1 similar comment
Collaborator
This was referenced Apr 28, 2026
Closed
Contributor
|
Thanks for the careful crash-safety work. This is an automated hermes-sweeper review: current
This behavior shipped in |
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.
Summary
SessionStore.rewrite_transcriptoverwrites the session's JSONL transcript in place:A crash between the truncate and the final flush — SIGKILL, OOM, power loss, a failing disk, or an exception raised during JSON serialization — leaves the transcript empty or partially written. This is the exact failure mode already acknowledged on the read side by
TestLoadTranscriptCorruptLines(GH-1193), which learned to skip corrupt lines after the fact. This PR closes the write-side hole so the corrupt state is never reachable in the first place.Why this matters
rewrite_transcriptsits on several hot gateway paths:/retry— strips and replays the last assistant turn (gateway/run.py:5930)/undo— rewrites history back to a prior user turn (gateway/run.py:5896)/compress— persists the compressed history into a new session (gateway/run.py:7085)gateway/run.py:4425)For sessions predating the SQLite layer, the JSONL is the only source of history — the existing comment at
gateway/session.py:1207explicitly says so: "legacy JSONL transcript (may contain more history than SQLite for sessions created before the DB layer was introduced)." A bad shutdown during any of the flows above silently wipes that session. Even on post-DB sessions, a corrupted JSONL defeats the replay fallback inload_transcript.Changes
utils.py— newatomic_jsonl_write(path, items, **dump_kwargs)helper. Mirrors the existingatomic_json_write/atomic_yaml_writepattern: tempfile +fsync+os.replace, with_preserve_file_mode/_restore_file_modeso Docker/NAS permission layouts from fix: preserve file permissions on atomic writes (Docker/NAS fix) #10618 are preserved, andBaseExceptioncleanup so stray.tmpfiles are never left behind onKeyboardInterrupt/SystemExit.gateway/session.py—SessionStore.rewrite_transcriptnow routes throughatomic_jsonl_write. No behavior change on the happy path.No API surface changes, no callers touched beyond the one in
rewrite_transcript.Alignment with recent merges
Same crash-safety pattern already shipped across the gateway:
fix(gateway): make Telegram DM topic config writes atomicfix(gateway/weixin): ensure atomic persistence for critical session statefix(tui): atomic config persistenceacp_adapter/session.py+hermes_state.py)rewrite_transcriptwas the obvious remaining gateway write that still truncated-in-place.Reproduction
Before the fix:
Tests
tests/hermes_cli/test_atomic_jsonl_write.py: 14 cases covering crash safety (BaseException+ mid-writeIOError), tempfile cleanup on success and failure, unicode, generators,dump_kwargsforwarding, empty input, string path support.tests/gateway/test_session.py::TestSessionStoreRewriteTranscriptAtomicity: 4 cases verifying that a crash duringrewrite_transcriptpreserves the prior transcript, that no.tmpfiles are left behind on success or failure, and that unicode survives the round trip.TestSessionStoreRewriteTranscriptandTestLoadTranscriptCorruptLinesremain green.Local run:
Risk
Very low.
atomic_jsonl_writeis a drop-in for the oldopen("w")+ write loop, and the helper matches the existingatomic_json_write/atomic_yaml_writestyle line-for-line. Behavior on the happy path is unchanged; only the failure window is closed.Checklist
utils.pyatomic_json_write)