fix(models): defer first save() until session has real state (#1171 follow-up)#1184
Closed
fix(models): defer first save() until session has real state (#1171 follow-up)#1184
Conversation
…ollow-up) #1182 stopped surfacing empty Untitled sessions in the sidebar but left the underlying disk-pile-up: every `/api/session/new` call still wrote a JSON file via the eager `s.save()` at the end of `new_session()`. Reload, click New Conversation, complete onboarding without sending a message — each one orphaned a file under `~/.hermes/webui/sessions/`. Fix: drop the eager save. A session lives in the in-memory `SESSIONS` dict from creation; the first disk write happens at the natural "this is now a real session" moment: 1. `_handle_chat_start` sets `pending_user_message` and calls `s.save()` before launching the streaming thread (api/routes.py:2806). This is the path for the very first user message. 2. btw and background-agent paths populate `title` + `messages` and call `.save()` themselves immediately after `new_session()` (api/routes.py:2710, 2752). Those continue to persist as before. The two tests that broke required only the targeted update they deserved: - `test_api_session_is_side_effect_free_for_stale_models` was reading the on-disk file straight after POST; updated to materialise the file from the API response when it isn't present, since the test's intent is verifying that a SUBSEQUENT GET doesn't rewrite the file. - The 7 new tests in `test_empty_session_no_disk_write.py` lock the new contract: - new_session() does not write to disk - the session lives in SESSIONS and is findable by get_session - an unsaved Untitled+0-msg session does not appear in all_sessions() output (the #1171 filter still applies) - save() writes when first invoked (post-message-append) - btw / background pattern still persists - five news in a row produce zero JSON files Crash-safety trade-off: if the process exits between create and first message, the unsaved session is lost. There were no messages to lose, so this is the correct semantics — only conversations that received real input get persisted. Builds on #1171 (server filter + button guard) and #1182 (boot early-exit, full-scan-fallback consistency). Local full suite: 2637 passed, 47 skipped, 1 unrelated pre-existing failure on macOS (test_sprint3 system-paths quirk). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
nesquena-hermes
added a commit
that referenced
this pull request
Apr 27, 2026
…30) (#1185) Merged as v0.50.230. 2685 tests passing. Browser QA 21/21. Closes the orphan-files leg of #1171. `new_session()` no longer writes an empty session to disk — the first disk write is deferred until the session has real state. Verified live: `POST /api/session/new` creates no `.json` file; session is findable by GET from in-memory SESSIONS dict. Attribution: original PR #1184 by @nesquena (Claude Code).
Collaborator
|
Merged as v0.50.230 via #1185. |
JKJameson
pushed a commit
to JKJameson/hermes-webui
that referenced
this pull request
Apr 29, 2026
…30) (nesquena#1185) Merged as v0.50.230. 2685 tests passing. Browser QA 21/21. Closes the orphan-files leg of nesquena#1171. `new_session()` no longer writes an empty session to disk — the first disk write is deferred until the session has real state. Verified live: `POST /api/session/new` creates no `.json` file; session is findable by GET from in-memory SESSIONS dict. Attribution: original PR nesquena#1184 by @nesquena (Claude Code).
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
Closes the orphan-files-on-disk leg of #1171. PR #1182 stopped surfacing empty Untitled sessions in the sidebar but left the underlying disk pile-up: every
/api/session/newstill wrote a JSON file via the eagers.save()at the end ofnew_session(). Reload, click New Conversation, complete onboarding without sending a message — each one left an orphan file under~/.hermes/webui/sessions/.What changed
api/models.py — drop the eager
s.save()fromnew_session(). The session lives in the in-memorySESSIONSdict from creation; the first disk write happens at the natural "this is now a real session" moment:/api/chat/start(first user message)pending_user_message, callss.save()before launching streaming (routes.py:2806)title+messages, callsephemeral.save()directly (routes.py:2710)title, callsbg.save()directly (routes.py:2752)get_session(sid)is unchanged — it checksSESSIONSfirst, so an unsaved session is still findable by ID for the brief window between create and first message.all_sessions()already filters Untitled+0-message sessions (#1171), so unsaved in-memory sessions don't surface in the sidebar even though they live inSESSIONS.Crash-safety trade-off
If the process exits between create and first message, the unsaved session is lost. There were no messages to lose, so this is the correct semantics — only conversations that received real input get persisted.
Tests
Updated (test_provider_mismatch.py:537) —
test_api_session_is_side_effect_free_for_stale_modelswas reading the on-disk file straight after POST. Updated to materialise the file from the API response when not present, since the test's actual intent is verifying that a subsequent GET doesn't rewrite the file.New (
tests/test_empty_session_no_disk_write.py) — 7 tests locking the new contract:new_session()does not write to diskSESSIONSand is findable viaget_sessionall_sessions()(the New Conversation always creates empty session even when current session is empty #1171 filter still applies)save()writes when first invoked (post-message-append)Local results
2637 passed, 47 skipped, 1 unrelated pre-existing failure (
test_sprint3.py::test_workspace_add_rejects_system_paths— macOS-only quirk, pre-exists on master).Builds on
🤖 Generated with Claude Code
EOF
)