Skip to content

ADFA-3085 | Fix ENOENT crash when saving chat sessions#1016

Merged
jatezzz merged 2 commits intostagefrom
fix/ADFA-3085-chat-storage-crash
Feb 27, 2026
Merged

ADFA-3085 | Fix ENOENT crash when saving chat sessions#1016
jatezzz merged 2 commits intostagefrom
fix/ADFA-3085-chat-storage-crash

Conversation

@jatezzz
Copy link
Collaborator

@jatezzz jatezzz commented Feb 25, 2026

Description

This PR fixes an android.system.ErrnoException: open failed: ENOENT crash occurring in ChatStorageManager.saveAllSessions. The crash was caused by a Time-of-check to time-of-use (TOCTOU) race condition when the OS file system (such as the FUSE cache) is out of sync after a directory deletion or recreation.

We now use idiomatic Kotlin runCatching blocks to defensively handle I/O operations on an individual file basis. By calling mkdirs() immediately before writing and safely catching any Throwable, we ensure that a single file failure does not crash the app or interrupt the entire saving loop.

Details

Before fix

Before fix
Screen.Recording.2026-02-25.at.4.23.08.PM.mov

After fix

After.fix.mov

Ticket

ADFA-3085

…lement runCatching to handle TOCTOU file system race conditions.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Release Notes - Chat Session Storage Crash Fix

Overview

Fixes an android.system.ErrnoException: open failed: ENOENT crash in chat session persistence caused by file system race conditions (TOCTOU - Time-Of-Check to Time-Of-Use).

Changes

  • Defensive error handling: Wrapped I/O operations in saveAllSessions() with Kotlin runCatching blocks to gracefully handle individual file failures
    • Session file enumeration now defaults to empty set on failure
    • Each session save wrapped with per-file error logging
    • Session deletion wrapped with pre-existence check and error logging
  • Partial failure tolerance: Single file failures no longer crash the app or interrupt the entire save/delete loop
  • Improved resilience: mkdirs() called immediately before each file write to handle file system state changes

Risk Assessment

⚠️ Inconsistent error handling approach: The saveSession() method still uses traditional try-catch while saveAllSessions() uses runCatching. Consider standardizing on one approach for maintainability.

⚠️ Multiple directory creation calls: mkdirs() is called for every session save regardless of whether the directory already exists. Consider caching the directory creation state or only calling on first failure to reduce system calls.

⚠️ Silent persistence failures: While errors are logged, there is no mechanism to surface persistence failures to the UI. Users may not be aware when their chat sessions fail to save.

⚠️ Existence check before deletion: The fileToDelete.exists() call adds a separate system call that introduces another potential race condition window, though practical impact is minimal.

Affected Functionality

  • Chat session persistence during normal operation and app lifecycle changes
  • Session file cleanup when sessions are removed from memory

Walkthrough

Modified ChatStorageManager.kt's saveAllSessions method to add defensive error handling around IO operations. File listing, session saving, and obsolete file deletion are now individually wrapped in runCatching blocks with logging, preventing crashes from partial IO failures while maintaining existing control flow.

Changes

Cohort / File(s) Summary
Error Handling in Chat Storage
app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt
Added per-call runCatching blocks around IO operations: listing existing session files with empty set fallback, saving sessions with parent directory creation, and deleting obsolete files with existence checks. All failures logged independently.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#997: Also modifies ChatStorageManager.kt's saveAllSessions to handle IO failures, but uses a canPersist guard with early returns rather than per-call error handling.

Suggested reviewers

  • Daniel-ADFA
  • itsaky-adfa

Poem

🐰 A rabbit hops through IO streams so wide,
With catching of errors as our trusted guide,
Each save and deletion now wrapped up tight,
No crashes or tumbles to darken the night! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the specific issue (ENOENT crash) and the component (chat sessions saving), directly matching the main change.
Description check ✅ Passed The description comprehensively relates to the changeset, explaining the TOCTOU race condition, the solution using runCatching, and includes relevant ticket references.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ADFA-3085-chat-storage-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt (1)

129-141: ⚠️ Potential issue | 🟠 Major

saveSession has the same TOCTOU gap the PR set out to fix.

canPersist() may return true, but the directory can disappear or go out of FUSE-cache sync before writeText() executes — yielding the identical ENOENT crash on the saveSession code path. The mkdirs() call added inside saveAllSessions is absent here, and catch (e: Exception) misses Error subclasses (e.g. OutOfMemoryError) unlike the runCatching used in the fixed path.

🛡️ Proposed fix — align `saveSession` with the new defensive pattern
     fun saveSession(session: ChatSession) {
         if (!canPersist(listOf(session))) return

-        try {
-            val sessionFile = File(storageDir, "${session.id}.txt")
-            // Serialize each message to a JSON string and join with newlines
-            val content = session.messages.joinToString("\n") { gson.toJson(it) }
-            sessionFile.writeText(content)
-        } catch (e: Exception) {
-            // It's good practice to handle potential I/O errors
-            Log.e(TAG, "Error saving session ${session.id}", e)
-        }
+        runCatching {
+            val sessionFile = File(storageDir, "${session.id}.txt")
+            sessionFile.parentFile?.mkdirs()
+            val content = session.messages.joinToString("\n") { gson.toJson(it) }
+            sessionFile.writeText(content)
+        }.onFailure { e ->
+            Log.e(TAG, "Error saving session ${session.id}", e)
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt`
around lines 129 - 141, saveSession has the same TOCTOU vulnerability as the
previous path: canPersist(session) may be true but storageDir can vanish before
sessionFile.writeText(...) causing ENOENT; fix by mirroring the defensive
pattern used in saveAllSessions: ensure storageDir.mkdirs() is called just
before creating/writing the File, and wrap the whole write operation in a
non-throwing runCatching (or equivalent) that catches Throwables (not just
Exception) so OutOfMemoryError and other Error subclasses are handled; update
references in saveSession (sessionFile, writeText, canPersist) to follow that
pattern and log the throwable on failure using the existing Log.e(TAG, ...).
🧹 Nitpick comments (1)
app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt (1)

116-122: exists() guard inside runCatching is redundant.

File.delete() returns false rather than throwing when the file is already gone, so the exists() pre-check buys nothing here. Calling delete() directly is simpler and still safe.

♻️ Simplification
         runCatching {
             val fileToDelete = File(storageDir, "$sessionId.txt")
-            if (fileToDelete.exists()) fileToDelete.delete()
+            fileToDelete.delete()
         }.onFailure { e ->
             Log.e(TAG, "Failed to delete session file $sessionId.txt", e)
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt`
around lines 116 - 122, In the ChatStorageManager code that deletes session
files (the block creating fileToDelete = File(storageDir, "$sessionId.txt"
inside the runCatching), remove the redundant fileToDelete.exists() guard and
call fileToDelete.delete() directly; keep the surrounding runCatching/onFailure
with Log.e(TAG, ...) so failures are still logged but simplify the deletion by
eliminating the exists() pre-check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt`:
- Around line 129-141: saveSession has the same TOCTOU vulnerability as the
previous path: canPersist(session) may be true but storageDir can vanish before
sessionFile.writeText(...) causing ENOENT; fix by mirroring the defensive
pattern used in saveAllSessions: ensure storageDir.mkdirs() is called just
before creating/writing the File, and wrap the whole write operation in a
non-throwing runCatching (or equivalent) that catches Throwables (not just
Exception) so OutOfMemoryError and other Error subclasses are handled; update
references in saveSession (sessionFile, writeText, canPersist) to follow that
pattern and log the throwable on failure using the existing Log.e(TAG, ...).

---

Nitpick comments:
In `@app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt`:
- Around line 116-122: In the ChatStorageManager code that deletes session files
(the block creating fileToDelete = File(storageDir, "$sessionId.txt" inside the
runCatching), remove the redundant fileToDelete.exists() guard and call
fileToDelete.delete() directly; keep the surrounding runCatching/onFailure with
Log.e(TAG, ...) so failures are still logged but simplify the deletion by
eliminating the exists() pre-check.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34dc0fb and 153fa33.

📒 Files selected for processing (1)
  • app/src/main/java/com/itsaky/androidide/agent/data/ChatStorageManager.kt

@jatezzz jatezzz requested a review from a team February 25, 2026 21:52
Copy link
Collaborator

@hal-eisen-adfa hal-eisen-adfa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jatezzz jatezzz merged commit a6772c7 into stage Feb 27, 2026
2 checks passed
@jatezzz jatezzz deleted the fix/ADFA-3085-chat-storage-crash branch February 27, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants