1pub mod acp_session_store;
6pub mod debounce;
7pub mod session_backend;
8pub mod session_sqlite;
9pub mod session_store;
10pub mod stall_watchdog;
11
12use std::path::Path;
13use std::sync::Arc;
14
15use crate::session_backend::SessionBackend;
16
17pub fn make_session_backend(
30 workspace_dir: &Path,
31 backend: &str,
32) -> std::io::Result<Arc<dyn SessionBackend>> {
33 match backend {
34 "jsonl" => {
35 let store = session_store::SessionStore::new(workspace_dir)?;
36 Ok(Arc::new(store))
37 }
38 "sqlite" => Ok(Arc::new(open_sqlite_with_jsonl_import(workspace_dir)?)),
39 other => {
40 ::zeroclaw_log::record!(
41 WARN,
42 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
43 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
44 .with_attrs(::serde_json::json!({"other": other})),
45 "Unknown session_backend ''; falling back to sqlite. \
46 Valid values: 'sqlite' (default), 'jsonl'."
47 );
48 Ok(Arc::new(open_sqlite_with_jsonl_import(workspace_dir)?))
49 }
50 }
51}
52
53fn open_sqlite_with_jsonl_import(
61 workspace_dir: &Path,
62) -> std::io::Result<session_sqlite::SqliteSessionBackend> {
63 let backend = session_sqlite::SqliteSessionBackend::new(workspace_dir)
64 .map_err(|e| std::io::Error::other(e.to_string()))?;
65 match backend.migrate_from_jsonl(workspace_dir) {
66 Ok(0) => {}
67 Ok(n) => ::zeroclaw_log::record!(
68 INFO,
69 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
70 &format!(
71 "session_backend=sqlite: imported {n} legacy JSONL session(s) from \
72 {}/sessions; renamed to *.jsonl.migrated.",
73 workspace_dir.display()
74 )
75 ),
76 Err(e) => ::zeroclaw_log::record!(
77 WARN,
78 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
79 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
80 .with_attrs(::serde_json::json!({"e": e.to_string()})),
81 "session_backend=sqlite: JSONL import skipped: . Existing JSONL \
82 sessions remain on disk; switch to session_backend = \"jsonl\" if \
83 you need them visible immediately."
84 ),
85 }
86 Ok(backend)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use tempfile::TempDir;
93 use zeroclaw_api::model_provider::ChatMessage;
94
95 fn user_msg(content: &str) -> ChatMessage {
96 ChatMessage::user(content)
97 }
98
99 #[test]
100 fn make_session_backend_jsonl_round_trips_through_session_store() {
101 let tmp = TempDir::new().unwrap();
102 let backend = make_session_backend(tmp.path(), "jsonl").unwrap();
103 backend.append("k1", &user_msg("hello-jsonl")).unwrap();
104 let loaded = backend.load("k1");
105 assert_eq!(loaded.len(), 1);
106 let jsonl = tmp.path().join("sessions").join("k1.jsonl");
108 assert!(jsonl.exists(), "jsonl file must be written under sessions/");
109 }
110
111 #[test]
112 fn make_session_backend_sqlite_round_trips_through_sqlite_db() {
113 let tmp = TempDir::new().unwrap();
114 let backend = make_session_backend(tmp.path(), "sqlite").unwrap();
115 backend.append("k1", &user_msg("hello-sqlite")).unwrap();
116 let loaded = backend.load("k1");
117 assert_eq!(loaded.len(), 1);
118 let db = tmp.path().join("sessions").join("sessions.db");
119 assert!(db.exists(), "sqlite db must be written under sessions/");
120 assert!(!tmp.path().join("sessions").join("k1.jsonl").exists());
122 }
123
124 #[test]
125 fn make_session_backend_unknown_value_falls_back_to_sqlite() {
126 let tmp = TempDir::new().unwrap();
127 let backend = make_session_backend(tmp.path(), "totally-not-a-backend").unwrap();
128 backend.append("k1", &user_msg("hello-fallback")).unwrap();
129 let db = tmp.path().join("sessions").join("sessions.db");
130 assert!(
131 db.exists(),
132 "unknown value must fall back to sqlite, not error"
133 );
134 }
135
136 #[test]
137 fn make_session_backend_sqlite_imports_legacy_jsonl_on_first_open() {
138 let tmp = TempDir::new().unwrap();
143 {
144 let jsonl = make_session_backend(tmp.path(), "jsonl").unwrap();
145 jsonl.append("legacy", &user_msg("from-jsonl")).unwrap();
146 }
147 let sqlite = make_session_backend(tmp.path(), "sqlite").unwrap();
148 let loaded = sqlite.load("legacy");
149 assert_eq!(
150 loaded.len(),
151 1,
152 "legacy JSONL session must hydrate via SQLite"
153 );
154 let jsonl_orig = tmp.path().join("sessions").join("legacy.jsonl");
156 let jsonl_migrated = tmp.path().join("sessions").join("legacy.jsonl.migrated");
157 assert!(!jsonl_orig.exists(), "original .jsonl should be renamed");
158 assert!(
159 jsonl_migrated.exists(),
160 ".jsonl.migrated rollback file should remain"
161 );
162 }
163}