feat: implement Hermes session manager with SQLite + JSONL support (Phase 6)

- Add hermes.rs session provider with dual-source scanning:
  SQLite (state.db) as primary, JSONL transcripts as fallback
- Dynamic schema discovery via PRAGMA table_info for SQLite resilience
- Use read_head_tail_lines for efficient JSONL metadata extraction
  (head 30 lines for metadata, tail 10 for last_active_at)
- Support both flat and nested JSONL message formats
- Add SQLite session loading and transactional deletion
- Register hermes in parallel session scan (thread::scope)
- Add "hermes" to frontend ProviderFilter type
- 7 unit tests covering JSONL parsing, SQLite source parsing, deletion
This commit is contained in:
Jason
2026-04-15 21:17:36 +08:00
parent 240969d8c7
commit e8953c286f
4 changed files with 621 additions and 7 deletions
+15 -6
View File
@@ -4,7 +4,7 @@ pub mod terminal;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use providers::{claude, codex, gemini, openclaw, opencode};
use providers::{claude, codex, gemini, hermes, openclaw, opencode};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -56,18 +56,20 @@ pub struct DeleteSessionOutcome {
}
pub fn scan_sessions() -> Vec<SessionMeta> {
let (r1, r2, r3, r4, r5) = std::thread::scope(|s| {
let (r1, r2, r3, r4, r5, r6) = std::thread::scope(|s| {
let h1 = s.spawn(codex::scan_sessions);
let h2 = s.spawn(claude::scan_sessions);
let h3 = s.spawn(opencode::scan_sessions);
let h4 = s.spawn(openclaw::scan_sessions);
let h5 = s.spawn(gemini::scan_sessions);
let h6 = s.spawn(hermes::scan_sessions);
(
h1.join().unwrap_or_default(),
h2.join().unwrap_or_default(),
h3.join().unwrap_or_default(),
h4.join().unwrap_or_default(),
h5.join().unwrap_or_default(),
h6.join().unwrap_or_default(),
)
});
@@ -77,6 +79,7 @@ pub fn scan_sessions() -> Vec<SessionMeta> {
sessions.extend(r3);
sessions.extend(r4);
sessions.extend(r5);
sessions.extend(r6);
sessions.sort_by(|a, b| {
let a_ts = a.last_active_at.or(a.created_at).unwrap_or(0);
@@ -88,10 +91,13 @@ pub fn scan_sessions() -> Vec<SessionMeta> {
}
pub fn load_messages(provider_id: &str, source_path: &str) -> Result<Vec<SessionMessage>, String> {
// OpenCode SQLite sessions use a "sqlite:" prefixed source_path
// SQLite sessions use a "sqlite:" prefixed source_path
if provider_id == "opencode" && source_path.starts_with("sqlite:") {
return opencode::load_messages_sqlite(source_path);
}
if provider_id == "hermes" && source_path.starts_with("sqlite:") {
return hermes::load_messages_sqlite(source_path);
}
let path = Path::new(source_path);
match provider_id {
@@ -100,7 +106,7 @@ pub fn load_messages(provider_id: &str, source_path: &str) -> Result<Vec<Session
"opencode" => opencode::load_messages(path),
"openclaw" => openclaw::load_messages(path),
"gemini" => gemini::load_messages(path),
"hermes" => Err("Hermes session loading not yet implemented".to_string()),
"hermes" => hermes::load_messages(path),
_ => Err(format!("Unsupported provider: {provider_id}")),
}
}
@@ -110,10 +116,13 @@ pub fn delete_session(
session_id: &str,
source_path: &str,
) -> Result<bool, String> {
// OpenCode SQLite sessions bypass the file-based deletion path
// SQLite sessions bypass the file-based deletion path
if provider_id == "opencode" && source_path.starts_with("sqlite:") {
return opencode::delete_session_sqlite(session_id, source_path);
}
if provider_id == "hermes" && source_path.starts_with("sqlite:") {
return hermes::delete_session_sqlite(session_id, source_path);
}
let root = provider_root(provider_id)?;
delete_session_with_root(provider_id, session_id, Path::new(source_path), &root)
@@ -151,7 +160,7 @@ fn delete_session_with_root(
"opencode" => opencode::delete_session(&validated_root, &validated_source, session_id),
"openclaw" => openclaw::delete_session(&validated_root, &validated_source, session_id),
"gemini" => gemini::delete_session(&validated_root, &validated_source, session_id),
"hermes" => Err("Hermes session deletion not yet implemented".to_string()),
"hermes" => hermes::delete_session(&validated_root, &validated_source, session_id),
_ => Err(format!("Unsupported provider: {provider_id}")),
}
}