Skip to content

feat(session): add FileSessionStorage for persistent sessions#134

Open
USEONGEE wants to merge 4 commits into
tuannvm:mainfrom
USEONGEE:feat/file-session-storage
Open

feat(session): add FileSessionStorage for persistent sessions#134
USEONGEE wants to merge 4 commits into
tuannvm:mainfrom
USEONGEE:feat/file-session-storage

Conversation

@USEONGEE
Copy link
Copy Markdown

@USEONGEE USEONGEE commented Mar 16, 2026

Summary

  • Add FileSessionStorage class that persists sessions to a JSON file (~/.codex-mcp/sessions.json)
  • Sessions survive MCP server restarts, enabling true multi-turn conversations across sessions
  • Introduce createSessionStorage() factory with env var controls for backwards compatibility

Features

  • Atomic writes: write-to-tmp + rename pattern prevents corruption on crash
  • Error recovery: corrupted JSON is backed up and storage starts fresh
  • TTL/LRU cleanup: sessions expire after 24h, max 100 sessions enforced
  • Schema versioning: v1 schema with migration path for future versions
  • File permissions: session file created with 0o600 (owner-only read/write)
  • Tmp file cleanup: leftover .tmp files cleaned up on startup

Environment Variables

  • CODEX_MCP_MEMORY_ONLY=true: fall back to in-memory storage (existing behavior)
  • CODEX_MCP_SESSION_FILE=/path/to/file.json: custom storage file path

Changes

  • src/session/file-storage.ts: New FileSessionStorage class implementing SessionStorage interface
  • src/__tests__/file-storage.test.ts: 22 tests covering CRUD, persistence, error recovery, TTL/LRU, atomic writes, schema loading
  • src/tools/handlers.ts: createSessionStorage() factory function replaces hardcoded InMemorySessionStorage
  • README.md: Document new environment variables and concurrency note

Test plan

  • npm run build passes
  • npm run lint passes
  • npm run format:check passes
  • npm test — all 93 tests pass (71 existing + 22 new)
  • Verified sessions persist across storage instance restarts in tests
  • Verified CODEX_MCP_MEMORY_ONLY=true falls back to in-memory storage

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional file-based persistent session storage (persists sessions across restarts) with memory-only toggle and configurable file path.
    • Enforces session expiration (TTL) and maximum session count to limit growth.
  • Documentation

    • Documented CODEX_MCP_MEMORY_ONLY and CODEX_MCP_SESSION_FILE.
    • Noted file-based storage is single-process oriented and may be overwritten by concurrent processes.
  • Tests

    • Added extensive tests covering persistence, recovery from corrupted or missing files, permissions, and edge cases.

USEONGEE and others added 3 commits March 16, 2026 21:50
SessionStorage 인터페이스를 구현하는 FileSessionStorage 클래스 추가.
원자적 쓰기, 에러 복구, TTL/LRU 관리 포함. 테스트 22개 통과.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: useongee <118250509+USEONGEE@users.noreply.github.com>
createSessionStorage() 팩토리 함수 도입.
CODEX_MCP_MEMORY_ONLY, CODEX_MCP_SESSION_FILE 환경변수 지원.
README에 환경변수 설명 및 동시성 경고 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: useongee <118250509+USEONGEE@users.noreply.github.com>
- Remove unused imports (unlink, mkdirSync)
- Replace NodeJS.ErrnoException with inline type
- Add eslint-disable for intentional control-regex pattern
- Apply Prettier formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: useongee <118250509+USEONGEE@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 16, 2026

Walkthrough

Adds a disk-backed session storage backend and wiring to choose it at runtime, plus docs and extensive tests validating persistence, recovery, concurrency, and TTL/LRU behaviors.

Changes

Cohort / File(s) Summary
Docs
README.md
Documented two env vars: CODEX_MCP_MEMORY_ONLY and CODEX_MCP_SESSION_FILE; noted single-process expectation for file-backed session file.
Session storage implementation
src/session/file-storage.ts
New FileSessionStorage class: versioned JSON file format, atomic .tmp writes + rename, debounced saves, corruption backup (.bak), schema validation, ISO timestamp parsing, TTL (24h) and max sessions (100), permission handling, flush() API.
Tests
src/__tests__/file-storage.test.ts
Comprehensive tests for lifecycle, persistence across instances, date/turn validation, corruption/recovery, backup behavior, permissions (0o600), concurrent/tmp-file handling, TTL/LRU enforcement, and flush/error scenarios.
Handlers integration
src/tools/handlers.ts
Added createSessionStorage() factory and switched handlers to use it; selects InMemorySessionStorage if CODEX_MCP_MEMORY_ONLY is set, otherwise uses FileSessionStorage.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handlers
    participant SessionStore as FileSessionStorage
    participant FS as Filesystem

    Client->>Handlers: request (create/ensure/get/addTurn/flush)
    Handlers->>SessionStore: call storage API
    alt Persist needed
        SessionStore->>FS: write `.tmp` JSON file (atomic write)
        FS-->>SessionStore: write OK
        SessionStore->>FS: rename `.tmp` -> `sessions.json`
        FS-->>SessionStore: rename OK
    else No persist / in-memory only
        SessionStore-->>Handlers: respond from memory
    end
    SessionStore-->>Handlers: return result
    Handlers-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰
A quiet file beneath the moon,
Sessions nest and timestamps tune,
Writes are safe, backups play,
TTL hops the old away,
A little rabbit guards the tray.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding FileSessionStorage for persistent sessions.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

@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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/session/file-storage.ts (3)

287-300: Silent failure on save errors loses data on crash.

executeSave() catches all errors silently (line 297-299). While the comment notes data remains in memory, if the process crashes after a failed save, changes are lost. Consider logging save failures to help diagnose issues in production.

♻️ Proposed fix to log save errors
   private async executeSave(): Promise<void> {
     try {
       const dir = dirname(this.filePath);
       mkdirSync(dir, { recursive: true, mode: 0o700 });

       const tmpPath = `${this.filePath}.${process.pid}.tmp`;
       const content = this.serialize();

       await writeFile(tmpPath, content, { mode: 0o600 });
       await rename(tmpPath, this.filePath);
-    } catch {
+    } catch (err) {
       // Non-fatal: data is still in memory
+      console.error('[FileSessionStorage] Save failed:', err);
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/file-storage.ts` around lines 287 - 300, The catch block in
executeSave() currently silences all errors causing silent save failures; modify
the catch to log the failure including this.filePath and the caught error
(error.message and/or error.stack) so save problems are visible in production;
use the class logger if one exists (e.g., this.logger or a provided logger),
otherwise fallback to console.error, and keep the existing behavior of not
throwing so in-memory data remains available.

336-345: enforceMaxSessions calls listSessions() which triggers cleanupExpiredSessions() again.

Inside enforceMaxSessions(), calling this.listSessions() (line 339) invokes cleanupExpiredSessions() a second time. This is redundant since enforceMaxSessions() is only called from createSession() and ensureSession(), both of which already call cleanupExpiredSessions() first.

♻️ Proposed fix to avoid double cleanup
 private enforceMaxSessions(): void {
   if (this.sessions.size <= this.maxSessions) return;

-  const sessions = this.listSessions();
+  // Sort sessions by lastAccessedAt (most recent first) without triggering cleanup again
+  const sessions = Array.from(this.sessions.values()).sort(
+    (a, b) => b.lastAccessedAt.getTime() - a.lastAccessedAt.getTime()
+  );
   const sessionsToDelete = sessions.slice(this.maxSessions);

   for (const session of sessionsToDelete) {
     this.sessions.delete(session.id);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/file-storage.ts` around lines 336 - 345, enforceMaxSessions
currently calls listSessions(), which re-invokes cleanupExpiredSessions and
causes redundant cleanup because createSession() and ensureSession() already
call cleanupExpiredSessions; change enforceMaxSessions to operate directly on
the in-memory map (this.sessions) instead of calling listSessions(): build an
array from this.sessions.values(), sort/order it the same way listSessions would
(if ordering matters), compute sessionsToDelete = array.slice(this.maxSessions)
and delete those by id from this.sessions; keep references to the functions
mentioned (enforceMaxSessions, listSessions, cleanupExpiredSessions,
createSession, ensureSession) to locate and update the code.

170-173: getCodexConversationId does not update lastAccessedAt.

All other read operations (getSession) update lastAccessedAt and trigger a save, but this method only reads without touching the session. This inconsistency could cause sessions to expire unexpectedly if they're only accessed via getCodexConversationId.

Consider whether this is intentional. If not:

♻️ Proposed fix to update lastAccessedAt
 getCodexConversationId(sessionId: string): string | undefined {
   const session = this.sessions.get(sessionId);
+  if (session) {
+    session.lastAccessedAt = new Date();
+    this.queueSave();
+  }
   return session?.codexConversationId;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/file-storage.ts` around lines 170 - 173, getCodexConversationId
reads a session but doesn't update lastAccessedAt like getSession does, causing
inconsistent expiration behavior; modify getCodexConversationId to mirror
getSession by retrieving the session from this.sessions, updating
session.lastAccessedAt = Date.now(), invoking the same save/persist method used
by getSession (e.g., saveSessions or persistSessions) to persist the timestamp,
and then return session.codexConversationId.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/handlers.ts`:
- Around line 559-566: The FileSessionStorage uses debounced saves via
queueSave() and pending writes can be lost on exit; modify
createSessionStorage/sessionStorage handling to register a graceful shutdown
handler (e.g., process.on('beforeExit'/'SIGINT'/'SIGTERM')) that calls the
storage.flush() and awaits completion before exiting. To make flush callable,
either add flush(): Promise<void> to the SessionStorage interface and implement
a no-op async flush in InMemorySessionStorage, or call (sessionStorage as
unknown as {flush?: () => Promise<void>}).flush() with a safe existence check
and await it; reference createSessionStorage, SessionStorage,
FileSessionStorage, InMemorySessionStorage, sessionStorage, queueSave(), and
flush() when making the change.

---

Nitpick comments:
In `@src/session/file-storage.ts`:
- Around line 287-300: The catch block in executeSave() currently silences all
errors causing silent save failures; modify the catch to log the failure
including this.filePath and the caught error (error.message and/or error.stack)
so save problems are visible in production; use the class logger if one exists
(e.g., this.logger or a provided logger), otherwise fallback to console.error,
and keep the existing behavior of not throwing so in-memory data remains
available.
- Around line 336-345: enforceMaxSessions currently calls listSessions(), which
re-invokes cleanupExpiredSessions and causes redundant cleanup because
createSession() and ensureSession() already call cleanupExpiredSessions; change
enforceMaxSessions to operate directly on the in-memory map (this.sessions)
instead of calling listSessions(): build an array from this.sessions.values(),
sort/order it the same way listSessions would (if ordering matters), compute
sessionsToDelete = array.slice(this.maxSessions) and delete those by id from
this.sessions; keep references to the functions mentioned (enforceMaxSessions,
listSessions, cleanupExpiredSessions, createSession, ensureSession) to locate
and update the code.
- Around line 170-173: getCodexConversationId reads a session but doesn't update
lastAccessedAt like getSession does, causing inconsistent expiration behavior;
modify getCodexConversationId to mirror getSession by retrieving the session
from this.sessions, updating session.lastAccessedAt = Date.now(), invoking the
same save/persist method used by getSession (e.g., saveSessions or
persistSessions) to persist the timestamp, and then return
session.codexConversationId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6acfa5b1-76af-45a8-9164-4b5b6350563b

📥 Commits

Reviewing files that changed from the base of the PR and between 00b6bf6 and ad1e810.

📒 Files selected for processing (4)
  • README.md
  • src/__tests__/file-storage.test.ts
  • src/session/file-storage.ts
  • src/tools/handlers.ts

Comment thread src/tools/handlers.ts
Comment on lines +559 to +566
function createSessionStorage(): SessionStorage {
if (process.env.CODEX_MCP_MEMORY_ONLY === 'true') {
return new InMemorySessionStorage();
}
return new FileSessionStorage(process.env.CODEX_MCP_SESSION_FILE || undefined);
}

const sessionStorage = createSessionStorage();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if InMemorySessionStorage has a flush method
ast-grep --pattern $'class InMemorySessionStorage {
  $$$
  flush($$$) {
    $$$
  }
  $$$
}'

Repository: tuannvm/codex-mcp-server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Find and examine the SessionStorage interface and implementations
fd -e ts -e js | xargs rg -l "interface SessionStorage|class FileSessionStorage|class InMemorySessionStorage" | head -20

Repository: tuannvm/codex-mcp-server

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Look for the session storage file
fd "storage\.(ts|js)" | head -10

Repository: tuannvm/codex-mcp-server

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Search for FileSessionStorage class definition
ast-grep --pattern $'class FileSessionStorage {
  $$$
  flush($$$) {
    $$$
  }
  $$$
}'

Repository: tuannvm/codex-mcp-server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Look for SessionStorage interface definition and check its methods
rg "interface SessionStorage" -A 30

Repository: tuannvm/codex-mcp-server

Length of output: 1908


🏁 Script executed:

#!/bin/bash
# Read the FileSessionStorage implementation
cat -n src/session/file-storage.ts

Repository: tuannvm/codex-mcp-server

Length of output: 11567


🏁 Script executed:

#!/bin/bash
# Check for shutdown handlers in the main handlers file
rg "beforeExit|SIGINT|SIGTERM|process\.on" src/tools/handlers.ts

Repository: tuannvm/codex-mcp-server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if there are any shutdown handlers elsewhere in the codebase
rg "beforeExit|SIGINT|SIGTERM" src/ --type ts

Repository: tuannvm/codex-mcp-server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Verify the createSessionStorage function in handlers.ts at lines 559-566
sed -n '555,570p' src/tools/handlers.ts

Repository: tuannvm/codex-mcp-server

Length of output: 509


🏁 Script executed:

#!/bin/bash
# Check context around this function - is it called during initialization?
rg "createSessionStorage|sessionStorage" src/tools/handlers.ts -B 2 -A 2

Repository: tuannvm/codex-mcp-server

Length of output: 1859


Missing graceful shutdown to flush pending saves.

FileSessionStorage uses debounced saves via queueSave(). If the process exits before a debounced save completes, data may be lost. The flush() method exists for this purpose but is never called on shutdown.

Add a shutdown handler to ensure pending saves complete before exit. Note: sessionStorage is typed as SessionStorage (the interface), which does not include flush(). To implement this, either add flush() to the SessionStorage interface with a no-op default in InMemorySessionStorage, or use a type assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/handlers.ts` around lines 559 - 566, The FileSessionStorage uses
debounced saves via queueSave() and pending writes can be lost on exit; modify
createSessionStorage/sessionStorage handling to register a graceful shutdown
handler (e.g., process.on('beforeExit'/'SIGINT'/'SIGTERM')) that calls the
storage.flush() and awaits completion before exiting. To make flush callable,
either add flush(): Promise<void> to the SessionStorage interface and implement
a no-op async flush in InMemorySessionStorage, or call (sessionStorage as
unknown as {flush?: () => Promise<void>}).flush() with a safe existence check
and await it; reference createSessionStorage, SessionStorage,
FileSessionStorage, InMemorySessionStorage, sessionStorage, queueSave(), and
flush() when making the change.

@hampsterx
Copy link
Copy Markdown
Contributor

Nice feature, the atomic write pattern and corruption recovery are well thought out. A few things to address before this is ready:

1. loadSync() over-aggressive corruption handling (file-storage.ts:209-217)

The catch block treats all non-ENOENT/EACCES errors as JSON corruption and renames the file. This means transient I/O errors, EISDIR from a misconfigured path, etc. would move the session file unnecessarily. Only JSON parse and schema validation failures should trigger the backup-and-reset path.

// Current: all errors → backup
} catch (err: unknown) {
  const error = err as { code?: string };
  if (error.code === 'ENOENT') return;
  if (error.code === 'EACCES') throw error;
  this.backupCorruptedFile(); // too broad
}

// Suggested: separate parse errors from filesystem errors
private loadSync(): void {
  let raw: string;
  try {
    raw = readFileSync(this.filePath, 'utf-8');
  } catch (err: unknown) {
    const error = err as { code?: string };
    if (error.code === 'ENOENT') return;
    throw error; // surface all other FS errors
  }
  try {
    const data: StorageFile = JSON.parse(raw);
    this.validateAndMigrate(data);
    // ... hydrate sessions ...
  } catch {
    this.backupCorruptedFile(); // only JSON/schema errors
  }
}

2. Silent error swallowing in executeSave() (file-storage.ts:297-299)

The empty catch {} makes save failures invisible. This also means flush() resolves successfully even when the write failed, which defeats its durability contract. At minimum, log the error:

-    } catch {
+    } catch (err) {
       // Non-fatal: data is still in memory
+      console.error('[FileSessionStorage] Save failed:', err);
     }

3. Session ID validation mismatch (file-storage.ts:44)

FileSessionStorage uses /^[^\x00-\x1f\x7f]+$/ (anything except control chars) while InMemorySessionStorage uses /^[a-zA-Z0-9_-]+$/ (strict alphanumeric). The Zod schema in types.ts also uses the strict pattern. A session ID with spaces would pass FileSessionStorage validation but fail InMemorySessionStorage, so switching backends via CODEX_MCP_MEMORY_ONLY=true could break clients. Should use the same pattern.


The CodeRabbit suggestions about enforceMaxSessions double cleanup and getCodexConversationId not updating lastAccessedAt are both inherited from InMemorySessionStorage (same code), so I wouldn't change them here.

The graceful shutdown / flush() on exit suggestion is valid in theory but the save fires on the next microtask so the data-loss window is tiny, and adding process.on handlers in an MCP stdio server could conflict with SDK shutdown handling. Fine as a follow-up.

…rity

- Separate loadSync() into two try/catch blocks: filesystem errors (EISDIR,
  EMFILE, etc.) now propagate instead of being treated as JSON corruption
- Remove silent catch in executeSave(): flush() now rejects on write failure,
  background saves log errors via console.error without auto-retry
- Add Date validation on load: sessions with invalid timestamps are skipped
  with a warning, preventing poisoned serialize() failures
- Scope tmp file cleanup to instance-local UUID suffix, preventing
  cross-instance interference in multi-process environments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: useongee <118250509+USEONGEE@users.noreply.github.com>
Copy link
Copy Markdown

@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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/session/file-storage.ts (2)

350-359: Minor efficiency note: listSessions() triggers another cleanup pass.

enforceMaxSessions calls listSessions() which internally calls cleanupExpiredSessions(). Since callers like createSession already call cleanupExpiredSessions() before enforceMaxSessions(), this results in redundant cleanup. This is inherited behavior and not blocking.

♻️ Optional: Add internal method without cleanup
+ private getSessionsSorted(): SessionData[] {
+   return Array.from(this.sessions.values()).sort(
+     (a, b) => b.lastAccessedAt.getTime() - a.lastAccessedAt.getTime()
+   );
+ }
+
  listSessions(): SessionData[] {
    this.cleanupExpiredSessions();
-   return Array.from(this.sessions.values()).sort(
-     (a, b) => b.lastAccessedAt.getTime() - a.lastAccessedAt.getTime()
-   );
+   return this.getSessionsSorted();
  }

  private enforceMaxSessions(): void {
    if (this.sessions.size <= this.maxSessions) return;

-   const sessions = this.listSessions();
+   const sessions = this.getSessionsSorted();
    const sessionsToDelete = sessions.slice(this.maxSessions);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/file-storage.ts` around lines 350 - 359, enforceMaxSessions
currently calls listSessions(), which invokes cleanupExpiredSessions again and
causes redundant work when callers (e.g., createSession) already cleaned up;
modify enforceMaxSessions to avoid that extra pass by either iterating
this.sessions directly (use this.sessions.values()/entries() to build sorted
list and then delete extras) or add a new internal helper like
listSessionsWithoutCleanup() that mirrors listSessions() logic but does not call
cleanupExpiredSessions(); update references in enforceMaxSessions to use the
direct iteration or new helper and keep function name enforceMaxSessions
unchanged.

165-168: Note: getCodexConversationId does not update lastAccessedAt.

Unlike getSession, this method doesn't update the LRU timestamp. This appears to be intentional to match InMemorySessionStorage behavior, but worth documenting if this is the intended design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/file-storage.ts` around lines 165 - 168, getCodexConversationId
reads a session without updating its LRU timestamp, which diverges from
getSession and can break last-accessed semantics; update the session's
lastAccessedAt the same way getSession does (set session.lastAccessedAt =
Date.now() and persist it back into this.sessions) before returning
session.codexConversationId, or if omission is intentional to match
InMemorySessionStorage, add a clear JSDoc comment above getCodexConversationId
explaining the deliberate behavior and rationale referencing
InMemorySessionStorage for future maintainers.
src/__tests__/file-storage.test.ts (2)

475-489: Permission-based tests may not work on Windows.

Tests using chmodSync to simulate permission errors rely on POSIX semantics and will pass differently (or may not simulate failures correctly) on Windows. This is acceptable for CI running on Linux but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/file-storage.test.ts` around lines 475 - 489, The
permission-based test 'flush should reject when write fails' uses chmodSync to
simulate POSIX permission errors which won't behave reliably on Windows; update
the test to skip or short-circuit on Windows (process.platform === 'win32') so
it only runs on POSIX CI. Locate the test case for FileSessionStorage (test name
"flush should reject when write fails") and add a platform guard at the top (or
convert to test.skip when process.platform === 'win32') before calling
mkdtempSync/tmpdir/chmodSync to avoid false failures on Windows.

506-506: Timing-based wait may be flaky in slow CI environments.

Using setTimeout with a fixed 100ms delay to wait for background save completion could be flaky under load. Consider using a more deterministic approach if this test becomes unstable.

♻️ Alternative: Poll for expected state
// Poll until console.error is called or timeout
const waitForError = async (spy: jest.SpyInstance, timeoutMs = 500) => {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    if (spy.mock.calls.length > 0) return;
    await new Promise(r => setTimeout(r, 10));
  }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/file-storage.test.ts` at line 506, Replace the flaky fixed
100ms sleep used to wait for background save (the line using await new Promise(r
=> globalThis.setTimeout(r, 100))) with a deterministic polling helper that
checks the test spy (e.g., the console.error or relevant jest.SpyInstance used
in this test) until it has been called or a short timeout elapses; implement a
helper like waitForError(spy, timeoutMs) that loops checking
spy.mock.calls.length and awaits a small delay (e.g., 10ms) between checks, then
call that helper in place of the one-shot setTimeout to ensure the test waits
for actual completion rather than an arbitrary sleep.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/session/file-storage.ts`:
- Around line 36-37: FileSessionStorage's sessionIdPattern
(/^[^\x00-\x1f\x7f]+$/) is laxer than InMemorySessionStorage's
(/^[a-zA-Z0-9_-]+$/) and can produce session IDs incompatible with the in-memory
backend; update the FileSessionStorage.sessionIdPattern to the stricter
/^[a-zA-Z0-9_-]+$/ (same as InMemorySessionStorage) so both storage
implementations validate session IDs identically, and run/adjust any unit tests
that assert session ID formats for the FileSessionStorage and
InMemorySessionStorage classes.

---

Nitpick comments:
In `@src/__tests__/file-storage.test.ts`:
- Around line 475-489: The permission-based test 'flush should reject when write
fails' uses chmodSync to simulate POSIX permission errors which won't behave
reliably on Windows; update the test to skip or short-circuit on Windows
(process.platform === 'win32') so it only runs on POSIX CI. Locate the test case
for FileSessionStorage (test name "flush should reject when write fails") and
add a platform guard at the top (or convert to test.skip when process.platform
=== 'win32') before calling mkdtempSync/tmpdir/chmodSync to avoid false failures
on Windows.
- Line 506: Replace the flaky fixed 100ms sleep used to wait for background save
(the line using await new Promise(r => globalThis.setTimeout(r, 100))) with a
deterministic polling helper that checks the test spy (e.g., the console.error
or relevant jest.SpyInstance used in this test) until it has been called or a
short timeout elapses; implement a helper like waitForError(spy, timeoutMs) that
loops checking spy.mock.calls.length and awaits a small delay (e.g., 10ms)
between checks, then call that helper in place of the one-shot setTimeout to
ensure the test waits for actual completion rather than an arbitrary sleep.

In `@src/session/file-storage.ts`:
- Around line 350-359: enforceMaxSessions currently calls listSessions(), which
invokes cleanupExpiredSessions again and causes redundant work when callers
(e.g., createSession) already cleaned up; modify enforceMaxSessions to avoid
that extra pass by either iterating this.sessions directly (use
this.sessions.values()/entries() to build sorted list and then delete extras) or
add a new internal helper like listSessionsWithoutCleanup() that mirrors
listSessions() logic but does not call cleanupExpiredSessions(); update
references in enforceMaxSessions to use the direct iteration or new helper and
keep function name enforceMaxSessions unchanged.
- Around line 165-168: getCodexConversationId reads a session without updating
its LRU timestamp, which diverges from getSession and can break last-accessed
semantics; update the session's lastAccessedAt the same way getSession does (set
session.lastAccessedAt = Date.now() and persist it back into this.sessions)
before returning session.codexConversationId, or if omission is intentional to
match InMemorySessionStorage, add a clear JSDoc comment above
getCodexConversationId explaining the deliberate behavior and rationale
referencing InMemorySessionStorage for future maintainers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11793858-23b5-4c73-babd-1b3104afe8a6

📥 Commits

Reviewing files that changed from the base of the PR and between ad1e810 and 716b646.

📒 Files selected for processing (2)
  • src/__tests__/file-storage.test.ts
  • src/session/file-storage.ts

Comment on lines +36 to +37
// eslint-disable-next-line no-control-regex
private readonly sessionIdPattern = /^[^\x00-\x1f\x7f]+$/;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check InMemorySessionStorage session ID validation pattern
rg -n 'sessionIdPattern|[a-zA-Z0-9_-]' --type ts -C3

Repository: tuannvm/codex-mcp-server

Length of output: 50381


🏁 Script executed:

# Get the exact content of file-storage.ts around lines 36-37
head -50 src/session/file-storage.ts | tail -20

Repository: tuannvm/codex-mcp-server

Length of output: 726


🏁 Script executed:

# Search for InMemorySessionStorage and its sessionIdPattern
fd -e ts . src/session/ | xargs grep -l "InMemorySessionStorage"

Repository: tuannvm/codex-mcp-server

Length of output: 90


🏁 Script executed:

# Get exact pattern from storage.ts
rg -n "sessionIdPattern" src/session/ -A 1 -B 1

Repository: tuannvm/codex-mcp-server

Length of output: 841


Unify session ID validation patterns across storage implementations.

The session ID validation differs between FileSessionStorage (line 37: /^[^\x00-\x1f\x7f]+$/) and InMemorySessionStorage (line 37: /^[a-zA-Z0-9_-]+$/). The file-based storage permits any non-control characters while the in-memory storage is restricted to alphanumerics, hyphens, and underscores. Sessions created in FileSessionStorage with characters like spaces or punctuation would fail validation if the backend is switched to InMemorySessionStorage, breaking interoperability.

Consider standardizing both implementations to the stricter pattern (/^[a-zA-Z0-9_-]+$/) to prevent data compatibility issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/file-storage.ts` around lines 36 - 37, FileSessionStorage's
sessionIdPattern (/^[^\x00-\x1f\x7f]+$/) is laxer than InMemorySessionStorage's
(/^[a-zA-Z0-9_-]+$/) and can produce session IDs incompatible with the in-memory
backend; update the FileSessionStorage.sessionIdPattern to the stricter
/^[a-zA-Z0-9_-]+$/ (same as InMemorySessionStorage) so both storage
implementations validate session IDs identically, and run/adjust any unit tests
that assert session ID formats for the FileSessionStorage and
InMemorySessionStorage classes.

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