Skip to content

fix(sessions): guard destructive deletes inside the store transaction - #124496

Closed
JoaoMarcos44 wants to merge 1 commit into
NousResearch:mainfrom
JoaoMarcos44:fix/123583-atomic-delete-guard-main
Closed

JoaoMarcos44 wants to merge 1 commit into
NousResearch:mainfrom
JoaoMarcos44:fix/123583-atomic-delete-guard-main

Conversation

@JoaoMarcos44

Copy link
Copy Markdown

What does this PR do?

Prevents user-facing session deletion from removing a row while a live turn or compression still owns it.

The recovery half of #123583 is already on main via #124117: if a row disappears under a live agent, the agent recreates it and replays the transcript. This PR addresses the remaining entry-side race at the SessionDB delete boundary.

Instead of preflighting a lease in each UI and then deleting in a second transaction, delete_session() and delete_sessions() gain an opt-in reject_active_write_guards flag. When enabled, the existing transcript write guards are checked inside the same writer transaction as the DELETE.

That closes the check/delete TOCTOU window and covers both:

  • live turn leases;
  • live compression locks.

The guard also covers recursive delegate children that the delete would cascade.

Related Issue

Refs #123583.

Agent-side recovery is already merged in #124117.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests only
  • ♻️ Refactor

Root cause / ownership

The store already owns the authoritative in-transaction guard in _check_transcript_write_guards().

A surface-level sequence like:

read lease -> decide safe -> begin delete

cannot guarantee safety because a turn can acquire the lease between the read and the delete. The invariant has to be enforced under the same writer transaction that removes the row.

Changes Made

  • SessionDB.delete_session(..., reject_active_write_guards=True)
    • checks target + cascaded delegate children before deletion;
    • rejects live turn leases and compression locks atomically.
  • SessionDB.delete_sessions(..., reject_active_write_guards=True)
    • applies the same invariant to the full destructive set;
    • the batch remains all-or-nothing.
  • User-facing destructive paths opt in:
    • hermes sessions delete;
    • interactive session browser delete;
    • sessions export --delete-after-verified;
    • CLI prune;
    • dashboard/web single + bulk delete and prune;
    • API-server delete;
    • TUI session.delete.
  • Internal cleanup callers keep the historical default unless they explicitly opt in.

Regression contract

tests/hermes_state/test_session_delete_write_guards.py contains two store-level invariants using a real temporary SessionDB:

  1. A live turn lease or compression lock makes protected single delete fail and leaves the row intact; releasing the guard allows deletion.
  2. A guarded recursive delegate child protects its parent from cascade deletion, and a protected member makes bulk deletion all-or-nothing.

The tests are deliberately at the store ownership boundary instead of duplicating the same lease logic in each UI test.

Relationship to existing work

@kshitijk4poor — #124117, building on @strzhao — #123641

Merged agent-side recovery for #123583. This PR does not duplicate it.

@shali10 — #123725

Addresses the same entry-side goal, but its current implementation checks session_turn_lease_holder() outside the delete transaction.

The review by @jonpol01 demonstrated concrete gaps in that approach:

  • check/delete TOCTOU;
  • compression locks not covered;
  • TUI delete not covered;
  • prune and verified-export paths remain unguarded.

This PR implements the ownership-boundary direction identified in that review: an opt-in guard inside delete_session / delete_sessions, reusing the existing store invariant rather than creating another lease reader.

No code from #123725 is copied.

Verification at 8f7c1b673ddd1fcb49a8d7e2020e5de847152c2b

  • base: main@c2af461706ae0dbab7271caf45bb9eefde4cdc53;
  • one commit ahead, zero behind at publication check;
  • 7 files changed;
  • no overlap with the 10 commits that advanced main during implementation;
  • local suite execution is not claimed in this environment; hosted CI is the execution gate.

Checklist

  • Read current contribution guidance.
  • Searched issues, PRs, reviews, symbols and current main.
  • Preserved prior contributor credit.
  • Kept the change at the controlling ownership boundary.
  • Added regression coverage.
  • Full pytest tests/ -q run not claimed.

Protect explicit session deletion at the SessionDB ownership boundary instead
of preflighting leases in each UI surface. The opt-in guard runs under the
same writer transaction as DELETE, covers turn leases, compression locks, and
cascaded delegate children, and makes bulk deletion all-or-nothing.

User-facing delete/prune paths opt in; internal cleanup keeps the existing
default semantics.

Refs NousResearch#123583
Related: NousResearch#123725
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 26, 2026
kshitijk4poor added a commit that referenced this pull request Sep 27, 2026
delete_session/delete_sessions cascade-delete delegate children, but the
write-guard check only looked at the root. A guarded delegate child could be
removed out from under its live turn, and in bulk delete an active id that
was also another selected root's delegate child was reported in
skipped_active while the cascade deleted it anyway.

Check {root, *delegate children} via a small _guarded_ids helper: single
delete refuses, bulk delete skips the root, so the cascade never touches a
guarded row. Ports the delegate-protection idea from #124496.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
@kshitijk4poor

Copy link
Copy Markdown

Thanks @JoaoMarcos44. The session-delete guard landed on main via #125157 (230f89b), built on #123725's in-transaction approach. Closing this one as superseded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists 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.

3 participants