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")