agentmux_srv/backend/history/adapter.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! History adapter trait and shared types.
5
6use serde::{Deserialize, Serialize};
7
8/// Error type for history operations.
9#[derive(Debug)]
10pub enum HistoryError {
11 Io(std::io::Error),
12 Json(serde_json::Error),
13 Other(String),
14}
15
16impl std::fmt::Display for HistoryError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 HistoryError::Io(e) => write!(f, "IO error: {}", e),
20 HistoryError::Json(e) => write!(f, "JSON error: {}", e),
21 HistoryError::Other(s) => write!(f, "{}", s),
22 }
23 }
24}
25
26impl From<std::io::Error> for HistoryError {
27 fn from(e: std::io::Error) -> Self {
28 HistoryError::Io(e)
29 }
30}
31
32impl From<serde_json::Error> for HistoryError {
33 fn from(e: serde_json::Error) -> Self {
34 HistoryError::Json(e)
35 }
36}
37
38/// A discovered file on disk (path + modification time).
39pub struct DiscoveredFile {
40 pub file_path: String,
41 pub mtime_ms: i64,
42}
43
44/// Lightweight metadata for the session list — extracted without full parsing.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SessionMeta {
47 pub session_id: String,
48 pub file_path: String,
49 pub provider: String,
50 pub model: String,
51 pub slug: String,
52 pub working_directory: String,
53 pub created_at: i64,
54 pub modified_at: i64,
55 pub message_count: u32,
56 pub first_user_message: String,
57 pub file_size_bytes: u64,
58 pub git_branch: String,
59 pub total_tokens: u64,
60 pub subagent_count: u32,
61 /// The account id (`db_accounts.id` / the `<account_id>` path
62 /// segment under `identities/`) this session was discovered under, if
63 /// any. Empty for sessions found not under an account's home (personal
64 /// `~/.claude`, the default `<shared>/providers/claude/projects`, or
65 /// legacy `~/.config/claude-*`) — those aren't attributable to a
66 /// specific identity. Lets `SessionIndex` answer "this agent's
67 /// sessions" in O(sessions for this identity) instead of a full scan;
68 /// see `docs/specs/SPEC_AGENT_IDENTITY_HISTORY_PERSISTENCE_PROTOCOL_2026_08_16.md`
69 /// §4.4. An agent's own id is resolved from this via
70 /// `db_agent_identity_links` (`Store::agent_identity_list_for_agent`),
71 /// not stored here directly — the same account can in principle be
72 /// shared, and this field reflects the filesystem fact (which account),
73 /// not a specific agent's claim on it.
74 #[serde(default)]
75 pub identity_id: String,
76 /// A conversation record with no timestamp comes before the first dated
77 /// one, so `created_at` (the first timestamp) isn't when the session
78 /// started — a time-window search can't rule the session out by it.
79 #[serde(default)]
80 pub starts_undated: bool,
81}
82
83/// What a discovery walk found, and what it should have looked at but
84/// couldn't (an unreadable directory, a file whose metadata failed). A
85/// search over a snapshot with problems can't claim to be complete: the
86/// session it's looking for may be exactly what was missed.
87#[derive(Default)]
88pub struct Discovery {
89 pub files: Vec<DiscoveredFile>,
90 pub problems: Vec<String>,
91}
92
93/// Full parsed session — produced on demand when user opens a conversation.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct HistorySession {
96 pub meta: SessionMeta,
97 pub messages: Vec<HistoryMessage>,
98 /// Records in the transcript that could not be read, so `messages` may be
99 /// missing some. A search over this session is then not a complete one.
100 #[serde(default)]
101 pub skipped_records: u32,
102}
103
104/// A single message in a conversation.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct HistoryMessage {
107 pub role: String,
108 pub content: String,
109 pub timestamp: i64,
110 pub tool_uses: Vec<ToolUseSummary>,
111}
112
113/// Summary of a tool call within an assistant message.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ToolUseSummary {
116 pub name: String,
117 pub argument_summary: String,
118}
119
120/// One implementation per CLI provider.
121pub trait HistoryAdapter: Send + Sync {
122 /// Provider identifier (e.g., "claude", "codex", "gemini", "kimi", "openclaw", "pi").
123 fn provider(&self) -> &str;
124
125 /// Discover all session file paths on disk.
126 /// Returns (file_path, mtime_ms) pairs, sorted by mtime descending.
127 fn discover_files(&self) -> Result<Vec<DiscoveredFile>, HistoryError>;
128
129 /// [`Self::discover_files`], plus what the walk couldn't read. An adapter
130 /// whose discovery can't fail part-way keeps this default.
131 fn discover(&self) -> Result<Discovery, HistoryError> {
132 Ok(Discovery { files: self.discover_files()?, problems: Vec::new() })
133 }
134
135 /// Extract lightweight metadata without full parsing.
136 fn extract_meta(&self, file_path: &str) -> Result<Option<SessionMeta>, HistoryError>;
137
138 /// Parse a single session file into a full HistorySession.
139 fn parse_file(&self, file_path: &str) -> Result<Option<HistorySession>, HistoryError>;
140}