Skip to main content

oxios_kernel/kernel_handle/
state_api.rs

1//! State API — data persistence, session management.
2
3use crate::state_store::{
4    PruneConfig, PruneThrottle, Session, SessionId, SessionSummary, StateStore,
5};
6use serde::{Serialize, de::DeserializeOwned};
7use std::sync::Arc;
8
9/// State management system calls.
10///
11/// All data persistence: file (JSON/Markdown) storage, session management.
12pub struct StateApi {
13    pub(crate) state_store: Arc<StateStore>,
14    /// Throttle for auto-prune to avoid excessive disk scans.
15    pub prune_throttle: PruneThrottle,
16}
17
18impl StateApi {
19    /// Create a new StateApi.
20    pub fn new(state_store: Arc<StateStore>) -> Self {
21        Self {
22            state_store,
23            prune_throttle: PruneThrottle::new(3600), // 1 hour cooldown
24        }
25    }
26    /// Save JSON data.
27    pub async fn save<T: Serialize>(
28        &self,
29        category: &str,
30        name: &str,
31        data: &T,
32    ) -> anyhow::Result<()> {
33        self.state_store.save_json(category, name, data).await
34    }
35
36    /// Save markdown content.
37    pub async fn save_markdown(
38        &self,
39        category: &str,
40        name: &str,
41        content: &str,
42    ) -> anyhow::Result<()> {
43        self.state_store
44            .save_markdown(category, name, content)
45            .await
46    }
47
48    /// Load JSON data.
49    pub async fn load<T: DeserializeOwned>(
50        &self,
51        category: &str,
52        name: &str,
53    ) -> anyhow::Result<Option<T>> {
54        self.state_store.load_json(category, name).await
55    }
56
57    /// Load markdown content.
58    pub async fn load_markdown(
59        &self,
60        category: &str,
61        name: &str,
62    ) -> anyhow::Result<Option<String>> {
63        self.state_store.load_markdown(category, name).await
64    }
65
66    /// Delete a file.
67    pub async fn delete(&self, category: &str, name: &str) -> anyhow::Result<bool> {
68        self.state_store.delete_file(category, name).await
69    }
70
71    /// List files in a category.
72    pub async fn list_category(&self, category: &str) -> anyhow::Result<Vec<String>> {
73        self.state_store.list_category(category).await
74    }
75
76    /// Commit all changes to git via the provided GitLayer.
77    ///
78    /// Returns:
79    /// - `Ok(None)` when git is disabled (no-op).
80    /// - `Ok(Some(info))` on a successful commit.
81    /// - `Err(...)` when the commit fails — callers must not conflate this
82    ///   with "git disabled", or a broken repo will silently look healthy.
83    pub fn commit_all(
84        &self,
85        git: &crate::git_layer::GitLayer,
86        message: &str,
87    ) -> anyhow::Result<Option<crate::git_layer::CommitInfo>> {
88        if !git.is_enabled() {
89            return Ok(None);
90        }
91        let info = git.commit_file(".", message)?;
92        Ok(Some(info))
93    }
94
95    /// Save session.
96    pub async fn save_session(&self, session: &Session) -> anyhow::Result<()> {
97        self.state_store.save_session(session).await
98    }
99
100    /// Load session.
101    pub async fn load_session(&self, id: &SessionId) -> anyhow::Result<Option<Session>> {
102        self.state_store.load_session(id).await
103    }
104
105    /// List sessions.
106    pub async fn list_sessions(&self) -> anyhow::Result<Vec<SessionSummary>> {
107        self.state_store.list_sessions().await
108    }
109
110    /// RFC-025 Phase 5: Load all sessions in full (for the promotion scanner).
111    pub async fn load_all_sessions(&self) -> anyhow::Result<Vec<Session>> {
112        self.state_store.load_all_sessions().await
113    }
114
115    /// Delete session.
116    pub async fn delete_session(&self, id: &SessionId) -> anyhow::Result<bool> {
117        self.state_store.delete_session(id).await
118    }
119
120    /// RFC-025: Move a session to a different Project (drag-to-reparent).
121    pub async fn move_session_to_project(
122        &self,
123        id: &SessionId,
124        project_id: Option<&str>,
125    ) -> anyhow::Result<bool> {
126        self.state_store
127            .move_session_to_project(id, project_id)
128            .await
129    }
130
131    /// Get workspace base path.
132    pub fn workspace_path(&self) -> &std::path::Path {
133        &self.state_store.base_path
134    }
135
136    /// Access the underlying StateStore (for backup/restore).
137    pub fn store(&self) -> &Arc<StateStore> {
138        &self.state_store
139    }
140
141    /// Prune sessions based on configuration.
142    ///
143    /// Removes sessions that exceed TTL or exceed the maximum count.
144    pub async fn prune_sessions(&self, config: &PruneConfig) -> anyhow::Result<usize> {
145        self.state_store.prune_sessions(config).await
146    }
147
148    /// Check if auto-prune should run (respects cooldown throttle).
149    pub fn should_auto_prune(&self) -> bool {
150        self.prune_throttle.should_prune()
151    }
152}