Skip to content

fix(models): defer first save() until session has real state (#1171 follow-up)#1184

Closed
nesquena wants to merge 1 commit intomasterfrom
fix/1171-no-empty-session-on-disk
Closed

fix(models): defer first save() until session has real state (#1171 follow-up)#1184
nesquena wants to merge 1 commit intomasterfrom
fix/1171-no-empty-session-on-disk

Conversation

@nesquena
Copy link
Copy Markdown
Owner

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/new 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 left an orphan file under ~/.hermes/webui/sessions/.

What changed

api/models.py — drop the eager s.save() from new_session(). The 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:

Path First save trigger
/api/chat/start (first user message) sets pending_user_message, calls s.save() before launching streaming (routes.py:2806)
btw (back-channel question) populates title + messages, calls ephemeral.save() directly (routes.py:2710)
background agent populates title, calls bg.save() directly (routes.py:2752)

get_session(sid) is unchanged — it checks SESSIONS first, 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 in SESSIONS.

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_models was 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:

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
)

…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).
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Merged as v0.50.230 via #1185.

@nesquena-hermes nesquena-hermes deleted the fix/1171-no-empty-session-on-disk branch April 27, 2026 23:59
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants