Skip to content

Plugins hear about every session delete and archive: on_session_delete hook (#124511, salvage #124988, #132639) - #140995

Merged
teknium1 merged 6 commits into
mainfrom
feat/hook-session-delete
Oct 18, 2026
Merged

teknium1 merged 6 commits into
mainfrom
feat/hook-session-delete

Conversation

@teknium1

@teknium1 teknium1 commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Plugins now get one on_session_delete(session_id, reason, profile, ...) call, after the commit, whenever a session is deleted or deliberately archived, from any surface.

Closes #124511. Salvages #124988 by @Finn763 (state-layer delete hook, commits kept with authorship) and folds in #132639 by @pieralukasz (archive as a "done" signal; Co-authored-by).

What plugins get

Field Value
session_id the session the caller targeted
reason delete, archive, empty_sweep, prune, ghost_prune, profile_repair
profile profile owning the store (default for the root home); dispatch runs under that profile's home, so its plugins get the event even when another profile's process did the delete
deleted_ids every removed or hidden row (delegate children, compression-chain links, bulk rows) when more than one went
sessions_dir the profile's sessions/ dir when the caller had one

Observer only: return ignored, a raising callback is logged and skipped, the delete still commits. Process-local delivery, no exactly-once guarantee (documented).

Changes

  • hermes_state_common.emit_session_deleted: one late-bound, fail-open seam into lifecycle.invoke_hook, called right after _execute_write returns and before media/transcript cleanup (which can raise).
  • Fired from delete_session, delete_sessions, delete_session_if_empty, delete_empty_sessions, prune_sessions, prune_empty_ghost_sessions, delete_moved_session (Finn's commits).
  • Added on top: mutate_runtime_session fires for delete and for a real archive flip. Desktop/dashboard single delete and archive go through this canonical receipt, not SessionDB.delete_session, so before this commit Desktop never fired. An exact retry replays the receipt and stays silent.
  • Archive (from feat(plugins): on_session_archived hook for deliberate session archives #132639): set_session_archived fires reason="archive" only on a real unarchived → archived flip. That covers TUI session.archive, deferred TUI archives and hermes sessions archive. Repeat archives, un-archives, the idle auto-archive sweep and profile adoption (notify=False) stay silent. One hook with a reason field replaces the separate on_session_archived hook.
  • Docs: hooks reference (table + section), plugin guide hook table, observer-hooks list, plugins.md observer list, zh-Hans rows.

Callers covered (grep of every DELETE FROM sessions / archive writer): CLI sessions delete|prune|archive, /exit --delete, quit-time empty discard, curses browser, auto-maintenance ghost prune and auto-prune, dashboard bulk delete / "Delete empty" / prune, Desktop DELETE + PATCH archived, TUI session.delete / session.archive, onboarding reset, gateway API server delete, gateway never-active prune, profile repair. Not fired: the api_server title-conflict rollback of a row inserted in the same transaction (commented), the seed-copy discard_unadmitted_session compensation path.

Validation

Check Result
tests/hermes_state/test_session_delete_hook.py (2 invariant tests, real SessionDB + real hook registry) head 2/2 pass; origin/main 0/2 (no events fired)
Neighbors: session archiving, archive RPC, repair-profiles, auto-archive (reactivation, lease, gateway lock, profile config), pinned/archived sidebar, web_server, adopted bot chat, hooks, plugins, branched-root delete, session API, housekeeping scope, export retention 669 passed, 0 failed (final head)
tests/hermes_state/test_hermes_state.py alone, head vs origin/main 297/297 both; 172 s vs 165 s (no slowdown)
scripts/check (incl. code health) ok; maybe_auto_archive moved to hermes_state_maintenance.py to keep hermes_state_sessions.py under 2000 lines

Cross-profile check: a default-profile process ran delete_moved_session on profiles/work/state.db. Only the work profile's plugin logged {"reason": "profile_repair", "profile": "work"}.

Live repro (real hermes processes, scratch HERMES_HOME, a scratch user plugin registering on_session_delete, three real chat sessions on Hermes-4-70B):

$ hermes sessions archive --title ok2 --yes
[delete-probe] {"session_id": "local-1b46…", "reason": "archive", "profile": "default"}
$ hermes sessions archive --title ok2 --yes        # already archived → no event
$ hermes sessions delete local-4440… --yes
[delete-probe] {"session_id": "local-4440…", "reason": "delete", "profile": "default", "sessions_dir": ".../home/.hermes/sessions"}
# hermes dashboard (Desktop's REST surface)
PATCH  /api/sessions/local-bc6a…  {"archived": true}
  → {"session_id": "local-bc6a…", "reason": "archive", "profile": "default"}
DELETE /api/sessions/local-bc6a…
  → {"session_id": "local-bc6a…", "reason": "delete", "profile": "default"}
DELETE (same request_id, exact retry) → receipt replayed, no event

On origin/main none of these produced a callback (hook not in VALID_HOOKS, no dispatch).

Infographic

on_session_delete

@teknium1 teknium1 added the ci-reviewed applied to manually approve dangerous changes label Oct 18, 2026
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 3d79bd7 — fix(plugins): on_session_delete review findings

debug info

CI timings

CI timings · View report · View job

Wall time 8m59s vs 13m1s (-31.0%). 2 job(s) slower, 15 faster, 3 unchanged.

  • OS-specific tests / Windows-only tests (arm64, 1/2): -134.0s
  • Python tests / Run tests (2/2): +106.0s
  • OS-specific tests / Windows-only tests (arm64, 2/2): -79.0s
  • OS-specific tests / Windows-only tests: -26.0s
  • Catalog tests / Catalog tests: -22.0s

@alt-glitch alt-glitch added area/sessions Session lifecycle, resume, persistence, history comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/feature New feature or request labels Oct 18, 2026
@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; all five previous findings are fixed, and no new blocking issue was found.

Summary

Adds on_session_delete notifications after committed deletes and deliberate archives across session entry points.

  • Plugins can hear when a session is deleted or deliberately archived.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Delete or deliberate archive] --> B[Commit database write]
    B --> C{Event to send?}
    C -->|No change or receipt replay| G[Return result]
    C -->|Yes| D[Bind owning profile home]
    D --> E[Invoke on_session_delete]
    E --> F[Restore previous profile home]
    F --> H[Run follow-up cleanup where needed]
    H --> G
Loading

Reviews (2) · Last reviewed commit: "fix(plugins): on_session_delete review f..." · Reviewed by Greptile

Comment thread hermes_state_runtime.py Outdated
Comment thread hermes_state_common.py Outdated
Comment thread hermes_state_sessions.py Outdated
Comment thread hermes_state_sessions.py Outdated
Comment thread website/docs/user-guide/features/hooks.md Outdated
@teknium1

Copy link
Copy Markdown
Collaborator Author

All five Greptile findings were valid and are fixed in e4ff3c7:

  1. Wrong profile receives delete events: emit_session_deleted now dispatches with the store's home bound (set_hermes_home_override) whenever it differs from the active home. Live check: a default-profile process ran delete_moved_session on profiles/work/state.db. Only the work profile's plugin logged {"reason": "profile_repair", "profile": "work"}, and the default plugin logged nothing.
  2. Archive events omit chain rows: both archive paths now pass the affected lineage ids (deleted_ids) and keep the requested id as session_id. The invariant test covers a root+tip chain on the SessionDB path and on the runtime path.
  3. Event state has no contract: write returns (result, SessionGoneEvent | None), a NamedTuple. The shared list is gone.
  4. Cleanup failures skip delete events: every delete path now emits right after _execute_write and before collect_retired_media / transcript removal.
  5. Hook reference contradicts itself: removed the leftover duplicate block.

Tests: 669 passed across 17 neighbor files, including test_hermes_state.py.

@greptileai review

@alt-glitch alt-glitch added the area/profiles Multi-profile isolation, HERMES_HOME scoping label Oct 18, 2026
@teknium1
teknium1 force-pushed the feat/hook-session-delete branch from e4ff3c7 to c028bc0 Compare October 18, 2026 08:15
Finn763 and others added 6 commits October 18, 2026 01:59
…e archives

Finn's state-layer hook missed the paths Desktop actually uses: single
delete and archive go through the canonical mutation receipt
(mutate_runtime_session), not SessionDB.delete_session, so a plugin never
heard about either. Archive (the "this is done" signal #132639 asked for)
fired nowhere.

- mutate_runtime_session fires after its write commits for operation
  'delete' and for a real unarchived -> archived sidebar flip; an exact
  retry replays the receipt and stays silent.
- set_session_archived fires reason="archive" on a real flip, so TUI
  session.archive, `hermes sessions archive` and deferred TUI archives are
  covered from one place. Repeats, un-archives, the idle auto-archive
  sweep and profile adoption (notify=False) stay silent.
- Payload is now session_id + reason + profile on every path (profile
  from the store's home), deleted_ids when more than one row went;
  primary id is the one the caller asked for, not the alphabetically
  first.
- One fail-open seam (emit_session_deleted) calls lifecycle.invoke_hook
  directly; the extra notify_session_deleted wrapper is gone.
- Tests trimmed to two invariants against the real hook registry.

Co-authored-by: Lucas Piera <pieralukasz@gmail.com>
hermes_state_sessions.py crossed the 2000-line target with the delete/archive
hook calls; the auto-archive wrapper belongs with the sweep it drives in
hermes_state_maintenance.py. Also drops the api_server rollback comment so
that facade does not grow.
- Dispatch under the store's own profile home: `sessions repair-profiles`
  deletes from another profile's store, and the event now reaches that
  profile's plugins instead of the launch profile's.
- Archive events list every hidden chain row (deleted_ids), on both the
  SessionDB and the runtime-receipt path; the requested id stays primary.
- Fire right after the commit, before media/transcript cleanup that can
  raise, so a cleanup failure no longer swallows the event.
- mutate_runtime_session's write returns (result, SessionGoneEvent | None)
  instead of filling a shared list.
- Drop the leftover duplicate hook reference block in hooks.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping area/sessions Session lifecycle, resume, persistence, history ci-reviewed applied to manually approve dangerous changes comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add an on_session_delete plugin hook (fire on every session-deletion path)

3 participants