Skip to main content

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}