feat: session garbage collection (#315)
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 14s

Add garbage_collect() method to SessionDB that cleans up empty and
trivial sessions based on age:
- Empty sessions (0 messages) older than 24h
- Trivial sessions (1-5 messages) older than 7 days
- Sessions with >5 messages kept indefinitely

Add `hermes sessions gc` CLI command with:
- --empty-hours (default: 24)
- --trivial-days (default: 7)
- --trivial-max (default: 5)
- --source filter
- --dry-run preview mode
- --yes skip confirmation

The dry-run flow: preview what would be deleted, ask for confirmation,
then execute. Handles child session FK constraints properly.

7 tests covering: empty/trivial deletion, active session protection,
substantial session preservation, dry-run, source filtering, and child
session handling.

Closes #315
This commit is contained in:
Alexander Whitestone
2026-04-13 17:30:39 -04:00
parent 992498463e
commit 69e10967bd
3 changed files with 248 additions and 1 deletions

View File

@@ -5004,7 +5004,7 @@ For more help on a command:
# =========================================================================
sessions_parser = subparsers.add_parser(
"sessions",
help="Manage session history (list, rename, export, prune, delete)",
help="Manage session history (list, rename, export, prune, gc, delete)",
description="View and manage the SQLite session store"
)
sessions_subparsers = sessions_parser.add_subparsers(dest="sessions_action")
@@ -5027,6 +5027,14 @@ For more help on a command:
sessions_prune.add_argument("--source", help="Only prune sessions from this source")
sessions_prune.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
sessions_gc = sessions_subparsers.add_parser("gc", help="Garbage-collect empty/trivial sessions")
sessions_gc.add_argument("--empty-hours", type=int, default=24, help="Delete empty (0-msg) sessions older than N hours (default: 24)")
sessions_gc.add_argument("--trivial-days", type=int, default=7, help="Delete trivial (1-5 msg) sessions older than N days (default: 7)")
sessions_gc.add_argument("--trivial-max", type=int, default=5, help="Max messages to consider trivial (default: 5)")
sessions_gc.add_argument("--source", help="Only GC sessions from this source")
sessions_gc.add_argument("--dry-run", action="store_true", help="Show what would be deleted without deleting")
sessions_gc.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
sessions_stats = sessions_subparsers.add_parser("stats", help="Show session store statistics")
sessions_rename = sessions_subparsers.add_parser("rename", help="Set or change a session's title")
@@ -5196,6 +5204,49 @@ For more help on a command:
size_mb = os.path.getsize(db_path) / (1024 * 1024)
print(f"Database size: {size_mb:.1f} MB")
elif action == "gc":
dry_run = getattr(args, "dry_run", False)
if dry_run:
counts = db.garbage_collect(
empty_older_than_hours=args.empty_hours,
trivial_max_messages=args.trivial_max,
trivial_older_than_days=args.trivial_days,
source=args.source,
dry_run=True,
)
print(f"[dry-run] Would delete {counts['total']} session(s):")
print(f" Empty (0 msgs, >{args.empty_hours}h old): {counts['empty']}")
print(f" Trivial (<={args.trivial_max} msgs, >{args.trivial_days}d old): {counts['trivial']}")
else:
# Preview first
preview = db.garbage_collect(
empty_older_than_hours=args.empty_hours,
trivial_max_messages=args.trivial_max,
trivial_older_than_days=args.trivial_days,
source=args.source,
dry_run=True,
)
if preview["total"] == 0:
print("Nothing to collect.")
else:
if not args.yes:
if not _confirm_prompt(
f"Delete {preview['total']} session(s) "
f"({preview['empty']} empty, {preview['trivial']} trivial)? [y/N] "
):
print("Cancelled.")
return
counts = db.garbage_collect(
empty_older_than_hours=args.empty_hours,
trivial_max_messages=args.trivial_max,
trivial_older_than_days=args.trivial_days,
source=args.source,
dry_run=False,
)
print(f"Collected {counts['total']} session(s):")
print(f" Empty: {counts['empty']}")
print(f" Trivial: {counts['trivial']}")
else:
sessions_parser.print_help()