[Skip to content](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#start-of-content)

You signed in with another tab or window. [Reload](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md) to refresh your session.You signed out in another tab or window. [Reload](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md) to refresh your session.You switched accounts on another tab or window. [Reload](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md) to refresh your session.Dismiss alert

{{ message }}

[salt-vrn](https://github.com/salt-vrn)/ **[hermes-session-maintenance](https://github.com/salt-vrn/hermes-session-maintenance)** Public

- [Notifications](https://github.com/login?return_to=%2Fsalt-vrn%2Fhermes-session-maintenance) You must be signed in to change notification settings
- [Fork\\
0](https://github.com/login?return_to=%2Fsalt-vrn%2Fhermes-session-maintenance)
- [Star\\
1](https://github.com/login?return_to=%2Fsalt-vrn%2Fhermes-session-maintenance)


## Collapse file tree

## Files

main

Search this repository(forward slash)` forward slash/`

/

# SKILL.md

Copy path

Blame

More file actions

Blame

More file actions

## Latest commit

![author](https://github.githubassets.com/images/gravatars/gravatar-user-420.png?size=40)

Leonid Zolotarev

[fix: remove 'stop gateway' advice — just schedule at 04:00 and let it…](https://github.com/salt-vrn/hermes-session-maintenance/commit/be6cebcf01584ce5220b8973aafd72657f1e01f2)

Open commit details

2 weeks agoJun 19, 2026

[be6cebc](https://github.com/salt-vrn/hermes-session-maintenance/commit/be6cebcf01584ce5220b8973aafd72657f1e01f2) · 2 weeks agoJun 19, 2026

## History

[History](https://github.com/salt-vrn/hermes-session-maintenance/commits/main/SKILL.md)

Open commit details

[View commit history for this file.](https://github.com/salt-vrn/hermes-session-maintenance/commits/main/SKILL.md) History

257 lines (177 loc) · 9.22 KB

/

# SKILL.md

Copy path

Top

## File metadata and controls

- Preview

- Code

- Blame


257 lines (177 loc) · 9.22 KB

[Raw](https://github.com/salt-vrn/hermes-session-maintenance/raw/refs/heads/main/SKILL.md)

Copy raw file

Download raw file

You must be signed in to make or propose changes

More edit options

Outline

Edit and raw actions

| name | hermes-session-maintenance |
| description | Use when sessions accumulate and memory/Disk usage grows. Monthly cleanup: close zombie sessions, prune old sessions, VACUUM state.db. Runs as a no-agent cron script — no LLM tokens spent. |
| version | 1.2.0 |
| author | salt-vrn |
| license | MIT |
| platforms | |     |
| --- |
| linux | |
| metadata | | hermes |
| --- |
| | tags | related\_skills |
| --- | --- |
| |     |     |     |     |     |     |     |
| --- | --- | --- | --- | --- | --- | --- |
| sessions | maintenance | cleanup | cron | memory | vacuum | sqlite | | |     |     |
| --- | --- |
| hermes-agent | hermes-administration | | | |

# Session Maintenance — Hermes Agent

[Permalink: Session Maintenance — Hermes Agent](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#session-maintenance--hermes-agent)

## Overview

[Permalink: Overview](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#overview)

Hermes stores every session and message in `state.db` (SQLite). Over months of use, the database grows — zombie sessions (never closed), old messages, and FTS indexes consume RAM and disk. This skill provides a battle-tested script that closes zombies, prunes old sessions, and reclaims disk space — all without spending a single LLM token.

**The problem it solves:**

- Agents accumulate sessions indefinitely
- FTS5 indexes grow with every message
- Zombie sessions (ended\_at IS NULL) leak memory
- SQLite never releases pages without VACUUM

## When to Use

[Permalink: When to Use](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#when-to-use)

- Your agent's memory usage is climbing month over month
- You see zombie sessions in `hermes sessions list` (no ended\_at)
- state.db is larger than expected for your usage
- You want a maintenance cron that runs silently without an LLM

**Don't use for:**

- Real-time session monitoring (use `hermes sessions list`)
- Deleting specific sessions (use `hermes sessions prune`)
- Debugging session state (use `hermes sessions inspect`)

## What the Script Does

[Permalink: What the Script Does](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#what-the-script-does)

For **each profile** (default + all named profiles):

### Step 1: Close Zombie Sessions

[Permalink: Step 1: Close Zombie Sessions](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#step-1-close-zombie-sessions)

Zombie sessions are sessions where `ended_at IS NULL` and `started_at` is older than 1 day. These sessions were never properly closed — the gateway crashed, the agent was killed mid-session, or a network interruption dropped the connection.

The script sets `ended_at = started_at` — we don't know the real end time, so we use the start time rather than fabricating a duration.

```
UPDATE sessions
SET ended_at = started_at
WHERE ended_at IS NULL
  AND started_at < (now - 86400)
```

> ⚠️ Direct SQL is used because `hermes` CLI has no "close zombie sessions" command. This is safe with WAL mode for concurrent reads, but the UPDATE may briefly lock the database.

**Why 1 day?** Active sessions under 24 hours old might genuinely still be running. The threshold avoids closing a long-running task.

### Step 2: Prune Old Ended Sessions

[Permalink: Step 2: Prune Old Ended Sessions](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#step-2-prune-old-ended-sessions)

Removes completed sessions older than 7 days using the built-in `hermes sessions prune` command. This cascades to messages and FTS indexes.

```
hermes -p "$PROFILE" sessions prune --older-than 7 --yes
```

**Why 7 days?** Enough history for debugging recent issues, old enough to prevent unbounded growth.

### Step 3: Backup + VACUUM

[Permalink: Step 3: Backup + VACUUM](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#step-3-backup--vacuum)

Before VACUUM, the script creates a timestamped backup of state.db in `$HERMES_HOME/backups/`. Backups older than 30 days are automatically cleaned up.

> ⚠️ **VACUUM requires exclusive lock.** If the gateway is actively writing, VACUUM will fail with `database is locked`. Schedule during low-activity hours (e.g., 04:00) when agents are idle — VACUUM takes seconds on small databases. The script creates a backup before VACUUM as a safety net. If VACUUM fails, data is safe — retry next month.

```
VACUUM
```

### Step 4: Time Format Sanity Check

[Permalink: Step 4: Time Format Sanity Check](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#step-4-time-format-sanity-check)

Before modifying any data, the script verifies that `started_at` contains unix seconds (not milliseconds or ISO strings). If the format is unexpected, the profile is skipped with a warning — preventing silent data corruption.

### Dry-Run Mode

[Permalink: Dry-Run Mode](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#dry-run-mode)

Run with `--dry-run` to preview what would be changed without modifying anything:

```
bash ~/.hermes/scripts/session-maintenance.sh --dry-run
```

### Report

[Permalink: Report](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#report)

The script outputs a summary per profile:

```
=== Session Maintenance Start ===
  default: zombies=3 pruned=12 freed=45MB remaining=150 sessions, 8420 messages
  frame: zombies=0 pruned=2 freed=8MB remaining=45 sessions, 2100 messages
=== Session Maintenance Done ===
```

## Installation

[Permalink: Installation](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#installation)

### 1\. Copy the script

[Permalink: 1. Copy the script](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#1-copy-the-script)

```
mkdir -p ~/.hermes/scripts
cp scripts/session-maintenance.sh ~/.hermes/scripts/
chmod +x ~/.hermes/scripts/session-maintenance.sh
```

### 2\. Test manually

[Permalink: 2. Test manually](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#2-test-manually)

```
bash ~/.hermes/scripts/session-maintenance.sh
```

### 3\. Schedule as a cron job (recommended)

[Permalink: 3. Schedule as a cron job (recommended)](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#3-schedule-as-a-cron-job-recommended)

Use Hermes built-in cron (no system crontab needed):

```
/cron create
```

Or via the agent:

> "Create a monthly cron job to run session-maintenance.sh on the 1st at 04:00"

**Recommended schedule:**`0 4 1 * *` — 1st of each month at 04:00 (low-traffic hours).

**Cron job settings:**

- `script: session-maintenance.sh`
- `no_agent: true` (no LLM tokens needed)
- `deliver: origin` (send report to your chat)

### 4\. Alternative: system crontab

[Permalink: 4. Alternative: system crontab](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#4-alternative-system-crontab)

```
crontab -e
# Add:
0 4 1 * * /bin/bash ~/.hermes/scripts/session-maintenance.sh >> ~/.hermes/cron/output/session-maintenance.log 2>&1
```

## Configuration

[Permalink: Configuration](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#configuration)

Edit the top of the script to adjust thresholds:

```
DAYS_ZOMBIE=1    # Close sessions older than N days with no ended_at
DAYS_PRUNE=7     # Delete ended sessions older than N days
```

**Conservative (less aggressive cleanup):**

```
DAYS_ZOMBIE=3
DAYS_PRUNE=30
```

**Aggressive (tight memory management):**

```
DAYS_ZOMBIE=1
DAYS_PRUNE=3
```

## How It Discovers Profiles

[Permalink: How It Discovers Profiles](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#how-it-discovers-profiles)

The script automatically finds all profiles:

1. **Default profile:**`$HERMES_HOME/state.db`
2. **Named profiles:**`$HERMES_HOME/profiles/*/state.db`

No configuration needed — new profiles are picked up automatically.

## Security & Reliability

[Permalink: Security & Reliability](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#security--reliability)

The script follows defensive bash practices:

- **No Python code injection** — all variables (`db_path`, `DAYS_ZOMBIE`) are passed via `sys.argv`, not interpolated into Python strings. Paths with quotes or special characters are safe.
- **flock** — prevents parallel execution. Two concurrent VACUUMs on the same SQLite file can cause corruption. The script acquires `/tmp/session-maintenance.lock` or exits immediately.
- **Dependency checks** — `hermes` and `python3` are verified with `command -v` before any work begins. No silent failures.
- **Safe prune parsing** — uses `(?<=Pruned )\d+` lookbehind to extract only the count, not arbitrary numbers from the output.
- **Non-negative freed** — guards against VACUUM increasing file size (rare but possible with fill factor).
- **printf '%b'** — instead of `echo -e` for portable escape sequence handling.

## Common Pitfalls

[Permalink: Common Pitfalls](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#common-pitfalls)

1. **VACUUM requires exclusive lock.** If the gateway is writing to state.db, VACUUM will fail with `database is locked`. Schedule during low-activity hours (e.g., 04:00). The script creates a backup before VACUUM as a safety net. If VACUUM fails — data is safe, retry next month.

2. **Running while the gateway is active.** The UPDATE for zombie sessions uses WAL-compatible writes (brief lock, not exclusive). VACUUM is the risky part — see pitfall #1.

3. **Pruning too aggressively.** If you set `DAYS_PRUNE=1`, you'll lose all session history. Start with 7 days, adjust based on your needs.

4. **Expecting immediate memory relief.** VACUUM reclaims _disk_ space. RAM relief comes from the gateway not loading pruned sessions into its working set. Restart the gateway after cleanup for maximum effect.

5. **Forgetting to make the script executable.**`chmod +x` is required, otherwise `bash script.sh` works but direct execution fails.

6. **Multiple Hermes installations.** If you have Hermes installed in both `~/.hermes` and another location, ensure `HERMES_HOME` is set correctly in the script.

7. **hermes not in PATH.** The script checks for `hermes` at startup, but if it's installed in a non-standard location, symlink it or adjust PATH before running.

8. **Parallel execution.** The script uses flock — if you see "Another instance is running", wait for it to finish or remove `/tmp/session-maintenance.lock` if you're sure no other instance is active.


## Verification Checklist

[Permalink: Verification Checklist](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#verification-checklist)

- [ ]  Script runs without errors: `bash ~/.hermes/scripts/session-maintenance.sh`
- [ ]  Report shows per-profile stats (zombies, pruned, freed)
- [ ]  Cron job is scheduled: `/cron list`
- [ ] `no_agent: true` is set (no LLM tokens consumed)
- [ ]  Next run date is correct
- [ ]  state.db size decreased after first run (compare with `du -h ~/.hermes/state.db`)

## One-Shot Recipes

[Permalink: One-Shot Recipes](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#one-shot-recipes)

### Manual cleanup right now

[Permalink: Manual cleanup right now](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#manual-cleanup-right-now)

```
bash ~/.hermes/scripts/session-maintenance.sh
```

### Check what would be cleaned (dry run)

[Permalink: Check what would be cleaned (dry run)](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#check-what-would-be-cleaned-dry-run)

```
import sqlite3, time

db = sqlite3.connect('/root/.hermes/state.db')
cutoff_zombie = time.time() - 86400
cutoff_prune = time.time() - (7 * 86400)

zombies = db.execute(
    'SELECT count(*) FROM sessions WHERE ended_at IS NULL AND started_at < ?',
    (cutoff_zombie,)
).fetchone()[0]

prunable = db.execute(
    'SELECT count(*) FROM sessions WHERE ended_at IS NOT NULL AND ended_at < ?',
    (cutoff_prune,)
).fetchone()[0]

db_size = db.execute('SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()').fetchone()[0]

print(f"Zombie sessions to close: {zombies}")
print(f"Ended sessions to prune: {prunable}")
print(f"Current DB size: {db_size / 1024 / 1024:.1f} MB")
```

### Force VACUUM only (without cleanup)

[Permalink: Force VACUUM only (without cleanup)](https://github.com/salt-vrn/hermes-session-maintenance/blob/main/SKILL.md#force-vacuum-only-without-cleanup)

```
python3 -c "
import sqlite3
conn = sqlite3.connect('$HOME/.hermes/state.db')
conn.execute('VACUUM')
conn.close()
print('VACUUM complete')
"
```

You can’t perform that action at this time.