mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(cli): accept session ID prefixes for session actions
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.
This commit is contained in:
parent
2b8fd9a8e3
commit
621fd80b1e
4 changed files with 126 additions and 6 deletions
|
|
@ -3103,7 +3103,11 @@ For more help on a command:
|
||||||
|
|
||||||
elif action == "export":
|
elif action == "export":
|
||||||
if args.session_id:
|
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:
|
if not data:
|
||||||
print(f"Session '{args.session_id}' not found.")
|
print(f"Session '{args.session_id}' not found.")
|
||||||
return
|
return
|
||||||
|
|
@ -3118,13 +3122,17 @@ For more help on a command:
|
||||||
print(f"Exported {len(sessions)} sessions to {args.output}")
|
print(f"Exported {len(sessions)} sessions to {args.output}")
|
||||||
|
|
||||||
elif action == "delete":
|
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:
|
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"):
|
if confirm.lower() not in ("y", "yes"):
|
||||||
print("Cancelled.")
|
print("Cancelled.")
|
||||||
return
|
return
|
||||||
if db.delete_session(args.session_id):
|
if db.delete_session(resolved_session_id):
|
||||||
print(f"Deleted session '{args.session_id}'.")
|
print(f"Deleted session '{resolved_session_id}'.")
|
||||||
else:
|
else:
|
||||||
print(f"Session '{args.session_id}' not found.")
|
print(f"Session '{args.session_id}' not found.")
|
||||||
|
|
||||||
|
|
@ -3140,10 +3148,14 @@ For more help on a command:
|
||||||
print(f"Pruned {count} session(s).")
|
print(f"Pruned {count} session(s).")
|
||||||
|
|
||||||
elif action == "rename":
|
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)
|
title = " ".join(args.title)
|
||||||
try:
|
try:
|
||||||
if db.set_session_title(args.session_id, title):
|
if db.set_session_title(resolved_session_id, title):
|
||||||
print(f"Session '{args.session_id}' renamed to: {title}")
|
print(f"Session '{resolved_session_id}' renamed to: {title}")
|
||||||
else:
|
else:
|
||||||
print(f"Session '{args.session_id}' not found.")
|
print(f"Session '{args.session_id}' not found.")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,32 @@ class SessionDB:
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
return dict(row) if row else None
|
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
|
# Maximum length for session titles
|
||||||
MAX_TITLE_LENGTH = 100
|
MAX_TITLE_LENGTH = 100
|
||||||
|
|
||||||
|
|
|
||||||
64
tests/hermes_cli/test_sessions_delete.py
Normal file
64
tests/hermes_cli/test_sessions_delete.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -361,6 +361,24 @@ class TestDeleteAndExport:
|
||||||
def test_delete_nonexistent(self, db):
|
def test_delete_nonexistent(self, db):
|
||||||
assert db.delete_session("nope") is False
|
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):
|
def test_export_session(self, db):
|
||||||
db.create_session(session_id="s1", source="cli", model="test")
|
db.create_session(session_id="s1", source="cli", model="test")
|
||||||
db.append_message("s1", role="user", content="Hello")
|
db.append_message("s1", role="user", content="Hello")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue