From 621fd80b1eb591dc06507e4432486e64a336d699 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sun, 15 Mar 2026 04:01:56 -0700 Subject: [PATCH] fix(cli): accept session ID prefixes for session actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve session IDs by exact match or unique prefix for sessions delete/export/rename so IDs copied from Preview Last Active Src ID ────────────────────────────────────────────────────────────────────────────────────────── Search for GitHub/GitLab source repositories for 11m ago cli 20260315_034720_8e1f [SYSTEM: The user has invoked the "minecraft-atm 1m ago cli 20260315_034035_57b6 1h ago cron cron_job-1_20260315_ [SYSTEM: The user has invoked the "hermes-agent- 9m ago cli 20260315_014304_652a 4h ago cron cron_job-1_20260314_ [The user attached an image. Here's what it cont 4h ago cli 20260314_233806_c8f3 [SYSTEM: The user has invoked the "google-worksp 1h ago cli 20260314_233301_b04f Inspect the opencode codebase for how it sends m 4h ago cli 20260314_232543_0601 Inspect the clawdbot codebase for how it sends m 4h ago cli 20260314_232543_8125 4h ago cron cron_job-1_20260314_ Reply with exactly: smoke-ok 4h ago cli 20260314_231730_aac9 4h ago cron cron_job-1_20260314_ [SYSTEM: The user has invoked the "hermes-agent- 4h ago cli 20260314_231111_3586 [SYSTEM: The user has invoked the "hermes-agent- 4h ago cli 20260314_225551_daff 5h ago cron cron_job-1_20260314_ [SYSTEM: The user has invoked the "google-worksp 4h ago cli 20260314_224629_a9c6 k_sze — 10:34 PM Just ran hermes update and I 5h ago cli 20260314_224243_544e 5h ago cron cron_job-1_20260314_ 5h ago cron cron_job-1_20260314_ 5h ago cron cron_job-1_20260314_ work even when the table view truncates them. Add SessionDB prefix-resolution coverage and a CLI regression test for deleting by listed prefix. --- hermes_cli/main.py | 24 ++++++--- hermes_state.py | 26 ++++++++++ tests/hermes_cli/test_sessions_delete.py | 64 ++++++++++++++++++++++++ tests/test_hermes_state.py | 18 +++++++ 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 tests/hermes_cli/test_sessions_delete.py diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 67f711b35..24458017c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3103,7 +3103,11 @@ For more help on a command: elif action == "export": if args.session_id: - data = db.export_session(args.session_id) + resolved_session_id = db.resolve_session_id(args.session_id) + if not resolved_session_id: + print(f"Session '{args.session_id}' not found.") + return + data = db.export_session(resolved_session_id) if not data: print(f"Session '{args.session_id}' not found.") return @@ -3118,13 +3122,17 @@ For more help on a command: print(f"Exported {len(sessions)} sessions to {args.output}") elif action == "delete": + resolved_session_id = db.resolve_session_id(args.session_id) + if not resolved_session_id: + print(f"Session '{args.session_id}' not found.") + return if not args.yes: - confirm = input(f"Delete session '{args.session_id}' and all its messages? [y/N] ") + confirm = input(f"Delete session '{resolved_session_id}' and all its messages? [y/N] ") if confirm.lower() not in ("y", "yes"): print("Cancelled.") return - if db.delete_session(args.session_id): - print(f"Deleted session '{args.session_id}'.") + if db.delete_session(resolved_session_id): + print(f"Deleted session '{resolved_session_id}'.") else: print(f"Session '{args.session_id}' not found.") @@ -3140,10 +3148,14 @@ For more help on a command: print(f"Pruned {count} session(s).") elif action == "rename": + resolved_session_id = db.resolve_session_id(args.session_id) + if not resolved_session_id: + print(f"Session '{args.session_id}' not found.") + return title = " ".join(args.title) try: - if db.set_session_title(args.session_id, title): - print(f"Session '{args.session_id}' renamed to: {title}") + if db.set_session_title(resolved_session_id, title): + print(f"Session '{resolved_session_id}' renamed to: {title}") else: print(f"Session '{args.session_id}' not found.") except ValueError as e: diff --git a/hermes_state.py b/hermes_state.py index 8945e195d..3f4715067 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -249,6 +249,32 @@ class SessionDB: row = cursor.fetchone() return dict(row) if row else None + def resolve_session_id(self, session_id_or_prefix: str) -> Optional[str]: + """Resolve an exact or uniquely prefixed session ID to the full ID. + + Returns the exact ID when it exists. Otherwise treats the input as a + prefix and returns the single matching session ID if the prefix is + unambiguous. Returns None for no matches or ambiguous prefixes. + """ + exact = self.get_session(session_id_or_prefix) + if exact: + return exact["id"] + + escaped = ( + session_id_or_prefix + .replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + ) + cursor = self._conn.execute( + "SELECT id FROM sessions WHERE id LIKE ? ESCAPE '\\' ORDER BY started_at DESC LIMIT 2", + (f"{escaped}%",), + ) + matches = [row["id"] for row in cursor.fetchall()] + if len(matches) == 1: + return matches[0] + return None + # Maximum length for session titles MAX_TITLE_LENGTH = 100 diff --git a/tests/hermes_cli/test_sessions_delete.py b/tests/hermes_cli/test_sessions_delete.py new file mode 100644 index 000000000..6f6d359b4 --- /dev/null +++ b/tests/hermes_cli/test_sessions_delete.py @@ -0,0 +1,64 @@ +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): + 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): + 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 diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index 81e922c7f..d77247936 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -361,6 +361,24 @@ class TestDeleteAndExport: def test_delete_nonexistent(self, db): assert db.delete_session("nope") is False + def test_resolve_session_id_exact(self, db): + db.create_session(session_id="20260315_092437_c9a6ff", source="cli") + assert db.resolve_session_id("20260315_092437_c9a6ff") == "20260315_092437_c9a6ff" + + def test_resolve_session_id_unique_prefix(self, db): + db.create_session(session_id="20260315_092437_c9a6ff", source="cli") + assert db.resolve_session_id("20260315_092437_c9a6") == "20260315_092437_c9a6ff" + + def test_resolve_session_id_ambiguous_prefix_returns_none(self, db): + db.create_session(session_id="20260315_092437_c9a6aa", source="cli") + db.create_session(session_id="20260315_092437_c9a6bb", source="cli") + assert db.resolve_session_id("20260315_092437_c9a6") is None + + def test_resolve_session_id_escapes_like_wildcards(self, db): + db.create_session(session_id="20260315_092437_c9a6ff", source="cli") + db.create_session(session_id="20260315X092437_c9a6ff", source="cli") + assert db.resolve_session_id("20260315_092437") == "20260315_092437_c9a6ff" + def test_export_session(self, db): db.create_session(session_id="s1", source="cli", model="test") db.append_message("s1", role="user", content="Hello")