//! Session metadata: id, title, workspace roots, and message/token counts, //! persisted as `session.json` per session directory. //! //! # Flow //! //! Created via [`Session::new`] → mutated in-memory → persisted via repository. //! //! # Components //! //! - `Session` struct — fields for all session metadata //! - `new` — timestamped constructor //! - `session_dir` / `conversation_path` — pure path computation use chrono::Utc; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; /// Metadata for one conversation session (distinct from the message /// history itself, which lives in `Conversation`/the msglog). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Session { /// Unique session identifier (validated against path traversal in `load`). pub id: String, /// Epoch-millis timestamp of creation (`Utc::now().timestamp_millis()`). pub created_at: i64, /// Epoch-millis timestamp of last update. pub updated_at: i64, /// Human-readable title for the conversation. pub title: String, /// Model identifier string, e.g. `"anthropic/claude-opus-4-8"`. pub model: String, /// Workspace root directories associated with this session. pub workspace_roots: Vec, /// Running count of messages in the conversation. pub message_count: u32, /// Running count of tokens consumed. pub token_count: u32, /// Soft-delete flag — archived sessions are hidden from the default list. pub archived: bool, /// Optional AI-generated conversation summary (used for compact context). pub summary: Option, } impl Session { /// Create a new session with the given id/title, defaulting the /// model, workspace root (current dir), and counters. pub fn new(id: String, title: String) -> Self { let now = Utc::now().timestamp_millis(); Session { id, created_at: now, updated_at: now, title, model: "anthropic/claude-opus-4-8".to_string(), workspace_roots: vec![std::env::current_dir().unwrap_or_default()], message_count: 0, token_count: 0, archived: false, summary: None, } } /// Compute this session's directory under `/sessions/`. pub fn session_dir(&self, base_dir: &Path) -> PathBuf { base_dir.join("sessions").join(&self.id) } /// Compute this session's `conversation.json` path. pub fn conversation_path(&self, base_dir: &Path) -> PathBuf { self.session_dir(base_dir).join("conversation.json") } }