Skip to content

fix(sessions): don't let the empty-session sweep delete an archived transcript - #96401

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:review-96092
Aug 27, 2026
Merged

kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:review-96092

Conversation

@kshitijk4poor

Copy link
Copy Markdown

Summary

Salvage of PR #96092 (@JoaoMarcos44) with a comment trim follow-up: the empty-session sweep can no longer hard-delete a session whose transcript survives as soft-archived (active=0) rows after a rewind or in-place compaction.

Root cause: count_empty_sessions() / delete_empty_sessions() selected on message_count = 0 alone. That counter tracks live (active=1) rows only, and two production paths deliberately reset it while keeping dropped turns on disk as active=0 — replace_messages(archive_dropped=True) (rewind, #82756) and archive_and_compact (in-place compaction). After a gateway reload stamps ended_at, such a row satisfied the sweep's gates and was hard-deleted together with its messages rows — silently, with no log line (#95868).

Changes

  • hermes_state.py: shared _EMPTY_SESSION_WHERE selector adds NOT EXISTS (SELECT 1 FROM messages WHERE messages.session_id = sessions.id) as the authority; message_count = 0 stays as a cheap prefilter. Same shape as all four sibling guards (delete_session_if_empty, prune_empty_ghost_sessions, list_never_active_keyed_sessions, find_recoverable_session).
  • hermes_cli/web_routers/sessions.py: endpoint docstring updated to match.
  • tests/hermes_state/test_empty_sweep_archived_transcript_95868.py: 5 new tests (3 fail without the fix).
  • Follow-up commit: trimmed the verbose #: comment and docstrings to avoid triple-duplication of the same explanation.

Validation

Before After
Rewound session (4 archived msgs) hard-deleted, unrecoverable survives sweep, resumable
Genuinely empty session swept swept (unchanged)
Count/delete agreement N/A pinned by test
  • 626 tests passed, 0 failed (hermes_state, empty_session_hygiene, web_server, session_system_prompt_dedup)
  • E2E: 20/20 checks passed (real SessionDB, real replace_messages + archive_and_compact)
  • Cherry-picked cleanly onto current main (110 commits behind, 1 unrelated file touch)

Closes #96092
Fixes #95868

@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/cli CLI entry point, hermes_cli/, setup wizard sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 27, 2026
JoaoMarcos44 and others added 3 commits August 27, 2026 19:53
…ranscript

`count_empty_sessions` / `delete_empty_sessions` — the dashboard's
"Delete empty (N)" affordance — defined "empty" as `sessions.message_count
= 0`. That column is a denormalized counter over the LIVE (`active = 1`)
rows only, and two production transcript-rewrite paths reset it on purpose
while keeping every dropped turn on disk as `active = 0`:

  * `replace_messages(..., archive_dropped=True)` — the rewind / edit /
    regenerate mode added in NousResearch#82756 so a taken-back turn stays recoverable.
  * `archive_and_compact` — in-place compaction, which archives the
    pre-compaction transcript under the same session id (NousResearch#38763).

A chat rewound to its first turn, or compacted with an empty live set,
therefore reports `message_count = 0` while still holding its entire
history — and those soft-archived rows are the only copy. A gateway reload
is what makes the row eligible: it stamps `ended_at` on every detached
session (`end_reason='ws_orphan_reap'`), satisfying the sweep's
`ended_at IS NOT NULL` gate. The next sweep then hard-deleted the session
row AND `DELETE FROM messages`, destroying the transcript silently.

Every other emptiness test in `hermes_state` already defends the counter
with a real `EXISTS (SELECT 1 FROM messages ...)` probe
(`delete_session_if_empty`, `prune_empty_ghost_sessions`,
`list_never_active_keyed_sessions`, `find_recoverable_session`). This
sweep was the only destructive path that trusted the counter alone. It now
uses the same probe, via one `_EMPTY_SESSION_WHERE` selector shared by the
count and the delete so the button's N and the sweep it triggers can never
disagree again. The counter stays as a cheap prefilter; `EXISTS` is the
authority.

Genuinely message-less rows are still swept — the feature is unchanged for
the case it was built for.

Fixes NousResearch#95868

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zTDHnWcBvQsJSX1fBLhuv
…esearch#95868 guards

Precision pass on the comments added by the previous commit: prompt.submit
reaches replace_messages(archive_dropped=True) with an empty prefix on a
confirmed ordinal-0 rewind, which is the production shape that lands a
populated session on message_count = 0. archive_and_compact normally
publishes at least a summary row, so it is pinned as defense in depth
rather than claimed as an equally reachable trigger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zTDHnWcBvQsJSX1fBLhuv
Deduplicate the explanation across the constant comment, count_empty_sessions
docstring, and delete_empty_sessions docstring. Keep the incident refs
(NousResearch#70516/NousResearch#80763/NousResearch#82756/NousResearch#95868) and the core WHY on the constant; cross-reference
from the methods.
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) August 27, 2026 14:24
@kshitijk4poor
kshitijk4poor merged commit 4a6e523 into NousResearch:main Aug 27, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop: active non-empty sessions silently hard-deleted from sessions table during gateway reload (detached_sessions>0, no delete_session call logged)

3 participants