feat: add file-based session persistence#2
Conversation
- Store session mappings in ~/.config/claude-code-mcp/sessions.json - Sessions persist across MCP server restarts - Auto-expire sessions after 24 hours - Clean expired sessions on load - Maintain LRU eviction (max 1000 sessions) This enables session continuity even when the MCP server is spawned fresh for each call (e.g., via npx).
📝 WalkthroughWalkthroughThe PR introduces a persistent, TTL-based session management system that replaces simple in-memory session storage. Sessions are now stored on disk in sessions.json with automatic expiration handling and LRU capacity eviction. Startup loads previous sessions while cleaning expired entries. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧹 Recent nitpick comments
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (3)**/*.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (.cursor/rules/agent.mdc)
Files:
**/*.{js,jsx,ts,tsx}📄 CodeRabbit inference engine (.cursor/rules/agent.mdc)
Files:
src/server.ts📄 CodeRabbit inference engine (AGENT.md)
Files:
🧠 Learnings (1)📚 Learning: 2026-01-14T23:01:49.563ZApplied to files:
🔇 Additional comments (6)
✏️ Tip: You can disable this entire section by setting 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. Comment |
Summary of ChangesHello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a robust file-based session persistence mechanism for the Claude CLI server. By storing session mappings in a dedicated JSON file, it resolves the problem of session data being lost across server restarts, particularly when the server is spawned as a fresh process via Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces file-based session persistence, which is a valuable addition for maintaining session continuity across server restarts. The implementation correctly handles loading, saving, and expiring sessions. However, I've identified a critical issue regarding potential race conditions if multiple server processes run concurrently, which could lead to data loss. There's also a high-severity performance concern with the use of synchronous file I/O that could block the event loop in a long-running server. I've provided detailed comments and suggestions to address these issues, along with some medium-severity recommendations for improving error logging and code clarity.
| function loadSessions(): void { | ||
| try { | ||
| if (existsSync(SESSION_FILE)) { | ||
| const data = readFileSync(SESSION_FILE, 'utf-8'); | ||
| const parsed = JSON.parse(data); | ||
| sessionMap = new Map(Object.entries(parsed)); | ||
| debugLog(`[Debug] Loaded ${sessionMap.size} sessions from ${SESSION_FILE}`); | ||
|
|
||
| // Clean up expired sessions on load | ||
| cleanExpiredSessions(); | ||
| } | ||
| } catch (e) { | ||
| debugLog('[Debug] Failed to load sessions file, starting fresh:', e); | ||
| sessionMap = new Map(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Save sessions from memory to file | ||
| */ | ||
| function saveSessions(): void { | ||
| try { | ||
| ensureSessionDir(); | ||
| const data = Object.fromEntries(sessionMap); | ||
| writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2)); | ||
| debugLog(`[Debug] Saved ${sessionMap.size} sessions to ${SESSION_FILE}`); | ||
| } catch (e) { | ||
| debugLog('[Debug] Failed to save sessions file:', e); | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of loading from and saving to sessions.json is not safe for concurrent execution. The pull request description mentions that each npx call spawns a new process. If two such processes run concurrently, they can create a race condition:
- Process A reads
sessions.json. - Process B reads
sessions.json. - Process A modifies its in-memory session map and writes the result to
sessions.json. - Process B modifies its in-memory session map and writes its result to
sessions.json, overwriting the changes from Process A.
This will lead to session data loss. To prevent this, you should implement a file locking mechanism around the read-modify-write cycle to ensure atomic updates to the session file. Libraries like proper-lockfile are well-suited for this purpose.
| function saveSessions(): void { | ||
| try { | ||
| ensureSessionDir(); | ||
| const data = Object.fromEntries(sessionMap); | ||
| writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2)); | ||
| debugLog(`[Debug] Saved ${sessionMap.size} sessions to ${SESSION_FILE}`); | ||
| } catch (e) { | ||
| debugLog('[Debug] Failed to save sessions file:', e); | ||
| } | ||
| } |
There was a problem hiding this comment.
The use of writeFileSync makes this a synchronous, blocking I/O operation. This function is called frequently (on every session creation/update via setSessionMapping, on expiration checks in getSessionMapping, and during cleanup). In a long-running server context, blocking the Node.js event loop for file I/O will cause significant performance degradation.
All file I/O should be performed asynchronously using fs.promises. This would require this function and its callers to become async and use await. Additionally, you might consider "debouncing" calls to saveSessions to avoid inefficiently writing to disk on every single modification.
| } catch (e) { | ||
| debugLog('[Debug] Failed to load sessions file, starting fresh:', e); | ||
| sessionMap = new Map(); | ||
| } |
There was a problem hiding this comment.
Failures in loading the session file are logged using debugLog, which is conditional on the MCP_CLAUDE_DEBUG environment variable. A failure to load sessions is a significant event that leads to the loss of all session continuity. This should be logged as a standard error to ensure it's always visible, regardless of debug settings.
| } catch (e) { | |
| debugLog('[Debug] Failed to load sessions file, starting fresh:', e); | |
| sessionMap = new Map(); | |
| } | |
| } catch (e) { | |
| console.error('[Error] Failed to load sessions file, starting fresh:', e); | |
| sessionMap = new Map(); | |
| } |
| } catch (e) { | ||
| debugLog('[Debug] Failed to save sessions file:', e); | ||
| } |
There was a problem hiding this comment.
Failures in saving the session file are logged using debugLog, which is conditional. A failure to save sessions can lead to data inconsistency between memory and disk and should be logged as a standard error to ensure the issue is always visible.
| } catch (e) { | |
| debugLog('[Debug] Failed to save sessions file:', e); | |
| } | |
| } catch (e) { | |
| console.error('[Error] Failed to save sessions file:', e); | |
| } |
| * Store a session mapping with LRU-style eviction and file persistence | ||
| */ |
There was a problem hiding this comment.
The comment describes "LRU-style eviction", but the implementation is actually FIFO (First-In, First-Out), as it removes the oldest entry by insertion order (sessionMap.keys().next().value). A true LRU cache evicts the least recently used item. To avoid confusion, please update the comment to accurately describe the FIFO behavior.
| * Store a session mapping with LRU-style eviction and file persistence | |
| */ | |
| * Store a session mapping with FIFO-style eviction and file persistence | |
🤖 Augment PR SummarySummary: Sessions now persist across MCP server restarts by storing parent-session → Claude CLI session mappings on disk. Changes:
🤖 Was this summary useful? React with 👍 or 👎 |
| if (existsSync(SESSION_FILE)) { | ||
| const data = readFileSync(SESSION_FILE, 'utf-8'); | ||
| const parsed = JSON.parse(data); | ||
| sessionMap = new Map(Object.entries(parsed)); |
There was a problem hiding this comment.
Because sessions.json is effectively external input, Object.entries(parsed) can populate sessionMap with values missing a valid updatedAt/claudeSessionId; then new Date(entry.updatedAt).getTime() becomes NaN and TTL expiry won’t work (or resumes can silently fail). Consider validating/coercing loaded entries before assigning to sessionMap.
🤖 Was this useful? React with 👍 or 👎
| try { | ||
| ensureSessionDir(); | ||
| const data = Object.fromEntries(sessionMap); | ||
| writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2)); |
There was a problem hiding this comment.
Since saveSessions() overwrites sessions.json directly, concurrent MCP processes (or a crash mid-write) can leave a partially-written file that resets sessions on the next startup. Consider making the persistence write atomic to reduce the chance of JSON corruption.
🤖 Was this useful? React with 👍 or 👎
- Store session mappings in ~/.config/claude-code-mcp/sessions.json - Sessions persist across MCP server restarts - Auto-expire sessions after 24 hours - Clean expired sessions on load - Maintain LRU eviction (max 1000 sessions) This enables session continuity even when the MCP server is spawned fresh for each call (e.g., via npx).
Summary
Sessions now persist across MCP server restarts by storing mappings in
~/.config/claude-code-mcp/sessions.json.Problem
When the MCP is invoked via
npx(as in OpenCode's config), each call spawns a fresh server process. The in-memory session map was lost between calls, breaking session continuity.Solution
~/.config/claude-code-mcp/sessions.jsonFile Format
{ "parent-session-123": { "claudeSessionId": "eff82f41-a021-4098-80a0-95117aa3f3e4", "updatedAt": "2026-01-14T23:15:00Z" } }Testing
Tested with Claude CLI
--resumeflag - sessions correctly persist and resume across server restarts.Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.