mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
* feat(sessions): full filter surface for prune + new bulk archive subcommand hermes sessions prune previously only supported --older-than N (integer days) and --source — no way to target a window like 'the last 5 hours' (e.g. a batch of CI smoke-test sessions), and no non-destructive option. - SessionDB.prune_sessions gains keyword filters that AND together: started_before/started_after epoch bounds, title_like, end_reason, cwd_prefix, min/max_messages, archived tri-state. Default call is byte-for-byte compatible (90-day cutoff, ended-only, source). - New SessionDB.list_prune_candidates (backs --dry-run + confirmation previews) and SessionDB.archive_sessions (bulk soft-hide via the existing set_session_archived lineage-aware path; nothing deleted). - CLI: prune gains --newer-than/--before/--after (durations like 5h/2d/1w, bare days, or ISO timestamps), --title, --end-reason, --cwd, --min/--max-messages, --include-archived, --dry-run. New 'hermes sessions archive' takes the same filters, requires at least one, and is idempotent. Both show a preview before confirming. - Dashboard /api/sessions/prune accepts the same filters + dry_run. - Docs: sessions.md + cli-commands.md updated. Filter parsing lives in hermes_cli/session_filters.py with unit tests; DB filters covered in tests/test_hermes_state.py. * feat(sessions): prune/archive filters for model, provider, user, chat, branch, tokens, cost, tool calls Extends the prune/archive filter surface to everything identifiable in the sessions table: - --model (substring on model slug), --provider (exact on billing_provider, case-insensitive), --user, --chat-id, --chat-type (exact), --branch (substring on git_branch), --min/--max-tokens (input+output), --min/--max-cost (USD, actual_cost_usd falling back to estimated_cost_usd), --min/--max-tool-calls. - SessionDB prune/archive/list_prune_candidates now share the filter kwargs via **filters into _prune_filter_where (unknown names raise TypeError); candidates listing + CLI preview now include the model. - Any attribute filter (except legacy --source) suppresses the implicit 90-day default so 'prune --model X' matches all ages. - Dashboard /api/sessions/prune passes the new fields through. - Docs + tests updated (7 new DB tests, 3 new parser tests).
130 lines
3.7 KiB
Python
130 lines
3.7 KiB
Python
import sys
|
|
|
|
|
|
def test_sessions_delete_accepts_unique_id_prefix(monkeypatch, capsys):
|
|
import hermes_cli.main as main_mod
|
|
import hermes_state
|
|
|
|
captured = {}
|
|
|
|
class FakeDB:
|
|
def resolve_session_id(self, session_id):
|
|
captured["resolved_from"] = session_id
|
|
return "20260315_092437_c9a6ff"
|
|
|
|
def delete_session(self, session_id, **kwargs):
|
|
captured["deleted"] = session_id
|
|
return True
|
|
|
|
def close(self):
|
|
captured["closed"] = True
|
|
|
|
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["hermes", "sessions", "delete", "20260315_092437_c9a6", "--yes"],
|
|
)
|
|
|
|
main_mod.main()
|
|
|
|
output = capsys.readouterr().out
|
|
assert captured == {
|
|
"resolved_from": "20260315_092437_c9a6",
|
|
"deleted": "20260315_092437_c9a6ff",
|
|
"closed": True,
|
|
}
|
|
assert "Deleted session '20260315_092437_c9a6ff'." in output
|
|
|
|
|
|
def test_sessions_delete_reports_not_found_when_prefix_is_unknown(monkeypatch, capsys):
|
|
import hermes_cli.main as main_mod
|
|
import hermes_state
|
|
|
|
class FakeDB:
|
|
def resolve_session_id(self, session_id):
|
|
return None
|
|
|
|
def delete_session(self, session_id, **kwargs):
|
|
raise AssertionError("delete_session should not be called when resolution fails")
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["hermes", "sessions", "delete", "missing-prefix", "--yes"],
|
|
)
|
|
|
|
main_mod.main()
|
|
|
|
output = capsys.readouterr().out
|
|
assert "Session 'missing-prefix' not found." in output
|
|
|
|
|
|
def test_sessions_delete_handles_eoferror_on_confirm(monkeypatch, capsys):
|
|
"""sessions delete should not crash when stdin is closed (non-TTY)."""
|
|
import hermes_cli.main as main_mod
|
|
import hermes_state
|
|
|
|
class FakeDB:
|
|
def resolve_session_id(self, session_id):
|
|
return "20260315_092437_c9a6ff"
|
|
|
|
def delete_session(self, session_id, **kwargs):
|
|
raise AssertionError("delete_session should not be called when cancelled")
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
|
|
monkeypatch.setattr(
|
|
sys, "argv",
|
|
["hermes", "sessions", "delete", "20260315_092437_c9a6"],
|
|
)
|
|
monkeypatch.setattr("builtins.input", lambda _prompt="": (_ for _ in ()).throw(EOFError))
|
|
|
|
main_mod.main()
|
|
|
|
output = capsys.readouterr().out
|
|
assert "Cancelled" in output
|
|
|
|
|
|
def test_sessions_prune_handles_eoferror_on_confirm(monkeypatch, capsys):
|
|
"""sessions prune should not crash when stdin is closed (non-TTY)."""
|
|
import hermes_cli.main as main_mod
|
|
import hermes_state
|
|
|
|
class FakeDB:
|
|
def list_prune_candidates(self, **kwargs):
|
|
return [
|
|
{
|
|
"id": "20260315_092437_c9a6ff",
|
|
"source": "cli",
|
|
"title": "old session",
|
|
"started_at": 0.0,
|
|
"ended_at": 1.0,
|
|
"message_count": 3,
|
|
"archived": 0,
|
|
}
|
|
]
|
|
|
|
def prune_sessions(self, **kwargs):
|
|
raise AssertionError("prune_sessions should not be called when cancelled")
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
|
|
monkeypatch.setattr(
|
|
sys, "argv",
|
|
["hermes", "sessions", "prune"],
|
|
)
|
|
monkeypatch.setattr("builtins.input", lambda _prompt="": (_ for _ in ()).throw(EOFError))
|
|
|
|
main_mod.main()
|
|
|
|
output = capsys.readouterr().out
|
|
assert "Cancelled" in output
|