Skip to content

Fix Archive Session for metadata-only cache hits - #2244

Merged
1 commit merged into
nesquena:masterfrom
franksong2702:franksong2702/archive-metadata-only-reload
May 14, 2026
Merged

1 commit merged into
nesquena:masterfrom
franksong2702:franksong2702/archive-metadata-only-reload

Conversation

@franksong2702

Copy link
Copy Markdown
Contributor

Thinking Path

Archive Session fails only after the target session has already been touched by a metadata-only path. The archive endpoint loads the session with get_session(sid), then mutates archived and calls save(). When the in-memory LRU already contains a _loaded_metadata_only=True instance, that cached stub is returned before a full disk load happens. Session.save() correctly refuses metadata-only instances under the #1558 guard because their messages=[] payload would risk overwriting the full transcript. So the bug is not the guard; it is that /api/session/archive mutates a cached metadata-only stub instead of upgrading it first.

What Changed

Fixes #2243.

Changed 3 files: 59 insertions, 1 deletion.

  • api/routes.py: reloads a cached metadata-only session with Session.load(sid) before mutating archived, then refreshes the session cache with the full instance. The existing CLI/imported-session fallback stays unchanged.
  • tests/test_metadata_save_wipe_1558.py: adds a regression test that places a metadata-only stub in SESSIONS, calls /api/session/archive, and asserts archive succeeds while the on-disk message history remains intact.
  • CHANGELOG.md: documents the user-visible Archive Session fix.

Why It Matters

Archiving is a common sidebar action. The pre-fix behavior looked like a dead click or Archive failed, even though the session existed and the requested mutation was valid. This restores the expected Archive/Restore behavior without weakening the data-loss guard added for #1558.

Verification

Local regression shape:

  1. Create a real session JSON with messages on disk.
  2. Load a metadata-only session stub.
  3. Put that stub into the session cache to reproduce the bad cache hit.
  4. Call POST /api/session/archive through the route handler.
  5. Verify the response is successful, archived=True persists, the cached object is now a full session, and all messages remain on disk.

Commands run:

/Users/xuefusong/hermes-webui/.venv_test/bin/python -m pytest -q tests/test_metadata_save_wipe_1558.py tests/test_issue2057_worktree_lifecycle.py::test_archive_worktree_session_reports_retained_worktree_without_cleanup
/Users/xuefusong/hermes-webui/.venv_test/bin/python -m py_compile api/routes.py api/models.py
git diff --check

Result: 13 passed plus py_compile and git diff --check passed.

Risks / Follow-ups

Low risk. This does not weaken the #1558 metadata-only save guard. Session.save() still refuses metadata-only instances; this route simply reloads the full persisted session before performing a valid archive mutation. If the full reload fails, the endpoint falls through to the existing not-found/CLI fallback behavior instead of saving the stub.

This PR intentionally does not change broader metadata-only cache semantics or session pin/move behavior.

Model Used

AI-assisted with OpenAI Codex, GPT-5. Human-directed scope and verification. No external code generation tools beyond local shell, tests, and GitHub CLI.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read this end-to-end on the PR worktree against origin/master — the diff is small (3 files, +59/-1) and the fix lands at the right layer.

What the change does

/api/session/archive previously called get_session(sid) which, in api/models.py:885-888, returns whatever instance is already pinned in the in-memory LRU regardless of how it was originally loaded:

with LOCK:
    if sid in SESSIONS:
        SESSIONS.move_to_end(sid)  # LRU: mark as recently used
        return SESSIONS[sid]

load_metadata_only() does not pin into SESSIONS (see get_session() at api/models.py:889-892 — metadata-only loads return the stub without populating the LRU), but the cache can be hot from a previous metadata_only=False load, and a stub can end up there indirectly through other code paths that hand stubs back to the cache (api/streaming.py:2910 builds one; api/routes.py:179, 975, 3226, 3278, 3517, 4461 all hand stubs to other helpers). The #1558 save guard at api/models.py:410-414 then rejects the eventual s.save() because _loaded_metadata_only=True, leaving the UI with Archive failed.

This PR's fix at api/routes.py:5114-5123:

if getattr(s, "_loaded_metadata_only", False):
    s = Session.load(sid)
    if s is None:
        raise KeyError(sid)
    with LOCK:
        SESSIONS[sid] = s

is exactly the same shape as the existing _clear_stale_stream_state() upgrade at api/routes.py:889-892:

if getattr(session, "_loaded_metadata_only", False):
    try:
        from api.models import get_session as _get_session
        session = _get_session(session.session_id, metadata_only=False)

so the pattern is already blessed for this kind of mutation route.

One small inconsistency worth noting

_clear_stale_stream_state() uses get_session(sid, metadata_only=False) to upgrade, which goes through get_session()'s LRU eviction-cap logic (api/models.py:894-900). The PR uses Session.load(sid) + a direct SESSIONS[sid] = s assignment, bypassing move_to_end and the SESSIONS_MAX eviction loop. Functionally fine — move_to_end is best-effort and SESSIONS_MAX will get enforced on the next eviction-aware path — but if you wanted to keep the two upgrade sites identical (which matters for #1558 follow-ups), reusing get_session(sid, metadata_only=False) here would be a one-line change. Not a blocker; just a consistency note.

Test coverage

tests/test_metadata_save_wipe_1558.py::test_archive_route_reloads_metadata_only_cached_session reproduces the precise bad state (stub forced into SESSIONS, route invoked through handle_post) and asserts both that the call returns 200 and that the cache entry is upgraded (_loaded_metadata_only is False, len(cached.messages) == 12). The disk-side assertion via Session.load(sid) confirms the persisted JSON is intact post-archive. Good shape — this would have caught the bug if it had existed when the test was written.

Other archive-route mutations that have the same shape

The same pattern exists at:

  • /api/session/pin (api/routes.py:5091-5103) — s.pinned = ...; s.save()
  • /api/session/rename (api/routes.py:4282-4294) — s.title = ...; s.save()
  • /api/personality/set (api/routes.py:4313-4337) — s.personality = ...; s.save()

All three call get_session(sid) then mutate + save() without the metadata-only upgrade. They are likely just as exposed as archive was. Worth filing a follow-up issue (or expanding this PR's scope, if you'd rather land it in one go) so we don't get a Pin failed / Rename failed report next.

Verdict

LGTM as a focused fix for #2243. Minor consistency suggestion above is optional. The regression test pins the contract; the #1558 save-guard stays intact; CHANGELOG is correctly placed under Unreleased → Fixed.

— maintainer

@franksong2702

Copy link
Copy Markdown
Contributor Author

Opened follow-up #2249 for the three adjacent metadata-only mutation routes you called out here: /api/session/pin, /api/session/rename, and /api/personality/set.

I kept this PR focused on Archive Session and tracked the adjacent-route work separately in #2248 so the review boundaries stay clear.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 18297f3 May 14, 2026
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 14, 2026
… 0.51.62) (#470)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.61` → `0.51.62` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.62`](https://github.com/nesquena/hermes-webui/releases/tag/v0.51.62)

[Compare Source](nesquena/hermes-webui@v0.51.61...v0.51.62)

##### What's Changed

- stage-355: 11-PR full sweep batch — metadata-only cache hit fixes + skill detail fix + phone UX + escaping + display-title projection + RFC update + test fixture hardening by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) in [#&#8203;2263](nesquena/hermes-webui#2263)
- Improve phone sidebar panel navigation by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2238](nesquena/hermes-webui#2238)
- fix: reconcile stale sidebar display titles by [@&#8203;dso2ng](https://github.com/dso2ng) in [#&#8203;2241](nesquena/hermes-webui#2241)
- Fix Archive Session for metadata-only cache hits by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2244](nesquena/hermes-webui#2244)
- Fix metadata-only cache hits in session mutation routes by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2249](nesquena/hermes-webui#2249)
- \[codex] Fix blank skill detail views by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2250](nesquena/hermes-webui#2250)
- docs(runtime): codify [#&#8203;1925](nesquena/hermes-webui#1925) adapter contract and migration gates by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2251](nesquena/hermes-webui#2251)
- \[codex] Show skill detail API errors by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2253](nesquena/hermes-webui#2253)
- \[codex] Escape model picker display text by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2255](nesquena/hermes-webui#2255)
- \[codex] Fix start.sh dotenv filtering load by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2257](nesquena/hermes-webui#2257)
- \[codex] Harden update-link git fixture by [@&#8203;franksong2702](https://github.com/franksong2702) in [#&#8203;2259](nesquena/hermes-webui#2259)

**Full Changelog**: <nesquena/hermes-webui@v0.51.61...v0.51.62>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/470
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
Fix Archive Session for metadata-only cache hits (franksong2702, fixes nesquena#2243)

# Conflicts:
#	CHANGELOG.md
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Fix Archive Session for metadata-only cache hits (franksong2702, fixes nesquena#2243)

# Conflicts:
#	CHANGELOG.md
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.

bug(session): archive fails when cached session is metadata-only

2 participants