Skip to content

Commit 40318a8

Browse files
committed
fix(storage): improve error handling in session storage operations
Fixes #5208, #5210, #5244 - Session storage error handling improvements. Changes: - list_sessions: Now properly collects and reports IO errors instead of silently ignoring them. Returns available sessions while logging errors for failed ones. - SessionManager: Only updates in-memory state after successful storage operations. If storage fails, the session is marked as modified for later retry. - Default impl: Improved panic message to help diagnose initialization failures (permissions, disk space).
1 parent 07906af commit 40318a8

3 files changed

Lines changed: 238 additions & 65 deletions

File tree

src/cortex-storage/src/sessions/storage.rs

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,48 +59,123 @@ impl SessionStorage {
5959
// ========================================================================
6060

6161
/// List all sessions, sorted by most recent first.
62+
///
63+
/// Returns both successfully loaded sessions and logs any errors encountered during listing.
64+
/// This ensures the caller is aware of any issues while still getting available sessions.
6265
pub async fn list_sessions(&self) -> Result<Vec<SessionSummary>> {
6366
let mut sessions = Vec::new();
67+
let mut errors: Vec<String> = Vec::new();
6468

6569
if !self.paths.sessions_dir.exists() {
6670
return Ok(sessions);
6771
}
6872

69-
let mut entries = fs::read_dir(&self.paths.sessions_dir).await?;
70-
while let Some(entry) = entries.next_entry().await? {
71-
let path = entry.path();
72-
if path.extension().map(|e| e == "json").unwrap_or(false) {
73-
match self.load_session_from_path(&path).await {
74-
Ok(session) => sessions.push(session.into()),
75-
Err(e) => warn!(path = %path.display(), error = %e, "Failed to load session"),
73+
let mut entries = fs::read_dir(&self.paths.sessions_dir).await.map_err(|e| {
74+
StorageError::Io(std::io::Error::new(
75+
e.kind(),
76+
format!(
77+
"Failed to read sessions directory {:?}: {}",
78+
self.paths.sessions_dir, e
79+
),
80+
))
81+
})?;
82+
83+
loop {
84+
let entry_result = entries.next_entry().await;
85+
match entry_result {
86+
Ok(Some(entry)) => {
87+
let path = entry.path();
88+
if path.extension().map(|e| e == "json").unwrap_or(false) {
89+
match self.load_session_from_path(&path).await {
90+
Ok(session) => sessions.push(session.into()),
91+
Err(e) => {
92+
let error_msg =
93+
format!("Failed to load session {:?}: {}", path.display(), e);
94+
errors.push(error_msg.clone());
95+
warn!(path = %path.display(), error = %e, "Failed to load session");
96+
}
97+
}
98+
}
99+
}
100+
Ok(None) => break,
101+
Err(e) => {
102+
let error_msg = format!("Failed to read directory entry: {}", e);
103+
errors.push(error_msg);
104+
warn!(error = %e, "Failed to read directory entry during session listing");
76105
}
77106
}
78107
}
79108

109+
// Log aggregate error count if any errors occurred
110+
if !errors.is_empty() {
111+
warn!(
112+
error_count = errors.len(),
113+
"Encountered {} error(s) while listing sessions",
114+
errors.len()
115+
);
116+
}
117+
80118
// Sort by updated_at descending (newest first)
81119
sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
82120
Ok(sessions)
83121
}
84122

85123
/// List all sessions synchronously.
124+
///
125+
/// Returns both successfully loaded sessions and logs any errors encountered during listing.
126+
/// This ensures the caller is aware of any issues while still getting available sessions.
86127
pub fn list_sessions_sync(&self) -> Result<Vec<SessionSummary>> {
87128
let mut sessions = Vec::new();
129+
let mut errors: Vec<String> = Vec::new();
88130

89131
if !self.paths.sessions_dir.exists() {
90132
return Ok(sessions);
91133
}
92134

93-
for entry in std::fs::read_dir(&self.paths.sessions_dir)? {
94-
let entry = entry?;
135+
let entries = std::fs::read_dir(&self.paths.sessions_dir).map_err(|e| {
136+
StorageError::Io(std::io::Error::new(
137+
e.kind(),
138+
format!(
139+
"Failed to read sessions directory {:?}: {}",
140+
self.paths.sessions_dir, e
141+
),
142+
))
143+
})?;
144+
145+
for entry_result in entries {
146+
let entry = match entry_result {
147+
Ok(e) => e,
148+
Err(e) => {
149+
let error_msg = format!("Failed to read directory entry: {}", e);
150+
errors.push(error_msg);
151+
warn!(error = %e, "Failed to read directory entry during session listing");
152+
continue;
153+
}
154+
};
155+
95156
let path = entry.path();
96157
if path.extension().map(|e| e == "json").unwrap_or(false) {
97158
match self.load_session_from_path_sync(&path) {
98159
Ok(session) => sessions.push(session.into()),
99-
Err(e) => warn!(path = %path.display(), error = %e, "Failed to load session"),
160+
Err(e) => {
161+
let error_msg =
162+
format!("Failed to load session {:?}: {}", path.display(), e);
163+
errors.push(error_msg);
164+
warn!(path = %path.display(), error = %e, "Failed to load session");
165+
}
100166
}
101167
}
102168
}
103169

170+
// Log aggregate error count if any errors occurred
171+
if !errors.is_empty() {
172+
warn!(
173+
error_count = errors.len(),
174+
"Encountered {} error(s) while listing sessions",
175+
errors.len()
176+
);
177+
}
178+
104179
sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
105180
Ok(sessions)
106181
}
@@ -438,7 +513,9 @@ impl SessionStorage {
438513

439514
impl Default for SessionStorage {
440515
fn default() -> Self {
441-
Self::new().expect("Failed to create session storage")
516+
Self::new().expect(
517+
"SessionStorage initialization failed - check directory permissions and disk space",
518+
)
442519
}
443520
}
444521

src/cortex-tui/src/session/manager.rs

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,18 @@ impl CortexSession {
179179
}
180180

181181
/// Updates token counts in metadata.
182+
///
183+
/// Saves metadata to disk. If save fails, the in-memory state is still
184+
/// updated but marked as modified for later retry.
182185
pub fn add_tokens(&mut self, input: i64, output: i64) {
183186
self.meta.add_tokens(input, output);
184187
if let Err(e) = self.storage.save_meta(&self.meta) {
185-
tracing::error!("Failed to save metadata: {}", e);
188+
tracing::error!(
189+
session_id = %self.meta.id,
190+
error = %e,
191+
"Failed to save metadata after token update"
192+
);
193+
self.modified = true;
186194
}
187195
}
188196

@@ -201,79 +209,135 @@ impl CortexSession {
201209

202210
/// Removes and returns the last exchange (user + assistant messages).
203211
/// Returns None if there are fewer than 2 messages.
212+
///
213+
/// Only updates in-memory state after successful storage operations.
214+
/// If storage fails, the messages are restored to maintain consistency.
204215
pub fn pop_last_exchange(&mut self) -> Option<Vec<cortex_core::widgets::Message>> {
205216
if self.messages.len() < 2 {
206217
return None;
207218
}
208219

220+
// Pop messages from memory temporarily
221+
let last = self.messages.pop();
222+
let prev = self.messages.pop();
223+
224+
// Build result from popped messages
209225
let mut result = Vec::new();
226+
let mut popped_messages = Vec::new();
210227

211-
// Pop the last message (should be assistant)
212-
if let Some(last) = self.messages.pop() {
213-
let role = match last.role.as_str() {
228+
if let Some(ref msg) = prev {
229+
let role = match msg.role.as_str() {
214230
"assistant" => cortex_core::widgets::MessageRole::Assistant,
215231
"user" => cortex_core::widgets::MessageRole::User,
216232
_ => cortex_core::widgets::MessageRole::System,
217233
};
218234
result.push(cortex_core::widgets::Message {
219235
role,
220-
content: last.content,
236+
content: msg.content.clone(),
221237
timestamp: None,
222238
is_streaming: false,
223239
tool_name: None,
224240
});
241+
popped_messages.push(msg.clone());
225242
}
226243

227-
// Pop the previous message (should be user)
228-
if let Some(prev) = self.messages.pop() {
229-
let role = match prev.role.as_str() {
244+
if let Some(ref msg) = last {
245+
let role = match msg.role.as_str() {
230246
"assistant" => cortex_core::widgets::MessageRole::Assistant,
231247
"user" => cortex_core::widgets::MessageRole::User,
232248
_ => cortex_core::widgets::MessageRole::System,
233249
};
234-
result.insert(
235-
0,
236-
cortex_core::widgets::Message {
237-
role,
238-
content: prev.content,
239-
timestamp: None,
240-
is_streaming: false,
241-
tool_name: None,
242-
},
243-
);
250+
result.push(cortex_core::widgets::Message {
251+
role,
252+
content: msg.content.clone(),
253+
timestamp: None,
254+
is_streaming: false,
255+
tool_name: None,
256+
});
257+
popped_messages.push(msg.clone());
244258
}
245259

246-
// Update metadata
247-
self.meta.message_count = self.messages.len() as u32;
248-
self.modified = true;
260+
// Try to save updated state to storage
261+
let rewrite_result = self.storage.rewrite_messages(&self.meta.id, &self.messages);
262+
263+
match rewrite_result {
264+
Ok(()) => {
265+
// Storage succeeded, update metadata
266+
self.meta.message_count = self.messages.len() as u32;
267+
268+
if let Err(e) = self.storage.save_meta(&self.meta) {
269+
tracing::error!(
270+
session_id = %self.meta.id,
271+
error = %e,
272+
"Failed to save metadata after undo - history is updated but metadata may be stale"
273+
);
274+
self.modified = true;
275+
}
249276

250-
// Save updated state (messages need to be rewritten)
251-
if let Err(e) = self.storage.rewrite_messages(&self.meta.id, &self.messages) {
252-
tracing::error!("Failed to rewrite messages after undo: {}", e);
253-
}
254-
if let Err(e) = self.storage.save_meta(&self.meta) {
255-
tracing::error!("Failed to save metadata after undo: {}", e);
256-
}
277+
Some(result)
278+
}
279+
Err(e) => {
280+
// Storage failed - restore messages to maintain consistency
281+
tracing::error!(
282+
session_id = %self.meta.id,
283+
error = %e,
284+
"Failed to rewrite messages after undo - restoring original state"
285+
);
286+
287+
// Restore in reverse order (prev was popped second, so push it first)
288+
if let Some(msg) = prev {
289+
self.messages.push(msg);
290+
}
291+
if let Some(msg) = last {
292+
self.messages.push(msg);
293+
}
257294

258-
Some(result)
295+
// Return None to indicate the operation failed
296+
None
297+
}
298+
}
259299
}
260300

261301
/// Internal method to add a message and persist it.
302+
///
303+
/// Only updates in-memory state after successful storage operations.
304+
/// This ensures consistency between disk and memory state.
262305
fn add_message_internal(&mut self, message: StoredMessage) -> &StoredMessage {
263-
// Append to storage first
264-
if let Err(e) = self.storage.append_message(&self.meta.id, &message) {
265-
tracing::error!("Failed to save message: {}", e);
266-
}
306+
// Try to append to storage first - only update memory state on success
307+
match self.storage.append_message(&self.meta.id, &message) {
308+
Ok(()) => {
309+
// Storage succeeded, now update metadata
310+
self.meta.increment_messages();
311+
312+
// Try to save metadata - if this fails, we still keep the message
313+
// since it was already persisted to history
314+
if let Err(e) = self.storage.save_meta(&self.meta) {
315+
tracing::error!(
316+
session_id = %self.meta.id,
317+
error = %e,
318+
"Failed to save metadata after message append - message is saved but metadata may be stale"
319+
);
320+
}
267321

268-
// Update metadata
269-
self.meta.increment_messages();
270-
if let Err(e) = self.storage.save_meta(&self.meta) {
271-
tracing::error!("Failed to save metadata: {}", e);
322+
// Add to in-memory list only after storage success
323+
self.messages.push(message);
324+
}
325+
Err(e) => {
326+
// Storage failed - log error but still add to memory for this session
327+
// This allows the conversation to continue even if persistence fails
328+
tracing::error!(
329+
session_id = %self.meta.id,
330+
error = %e,
331+
"Failed to save message to storage - message exists only in memory"
332+
);
333+
334+
// Still add to memory so the conversation can continue
335+
self.messages.push(message);
336+
self.modified = true; // Mark as modified since we have unsaved changes
337+
}
272338
}
273339

274-
// Add to in-memory list
275-
self.messages.push(message);
276-
self.messages.last().unwrap()
340+
self.messages.last().expect("message was just added")
277341
}
278342

279343
/// Converts messages to API format for completion requests.

0 commit comments

Comments
 (0)