#19528: feat: directory-per-session store to eliminate monolithic JSON bottleneck
size: L
Cluster:
Session Management Enhancements
## Problem
The session store is a single monolithic JSON file (`sessions.json`) that grows to 80MB+ with 650+ sessions. Every read/write operation:
- Parses the entire file (~80ms+ for `JSON.parse` alone)
- Serializes and rewrites the entire file atomically
- Requires global file locking, causing contention and orphaned `.tmp` files
This is O(n) for what should be O(1) operations.
## Solution: Directory-per-session layout
```
sessions.d/
agent--main--telegram--direct--james/
meta.json # ~200 bytes
agent--main--cron--14bbd904/
meta.json
```
### Key changes in `src/config/sessions/store.ts`:
- **Auto-detection:** If `sessions.d/` directory exists, uses directory mode. Otherwise falls back to legacy JSON (backward compatible).
- **Diff-based writes:** `updateSessionStore()` snapshots the store before mutation, then only writes changed entries and deletes removed ones — no more full rewrites.
- **Per-entry I/O:** `readSessionUpdatedAt()` reads a single `meta.json` instead of parsing the entire store.
- **Atomic writes:** Each `meta.json` uses temp-file + rename, same as the existing pattern.
- **Migration:** `migrateSessionStoreToDirectory(storePath)` splits an existing JSON file into the directory layout, backs up the original file, and is safe to call multiple times.
### Performance characteristics:
| Operation | Before (JSON) | After (Directory) |
|-----------|--------------|-------------------|
| Read one session | O(n) parse entire file | O(1) read single meta.json |
| Write one session | O(n) serialize + write all | O(1) write single meta.json |
| List sessions | O(n) parse | O(1) readdir |
| Locking contention | Global file lock | Per-session potential |
### Migration path:
1. No breaking changes — existing `sessions.json` continues working as-is
2. Call `migrateSessionStoreToDirectory(storePath)` to opt in
3. After migration, original JSON is backed up as `sessions.json.pre-directory-migration.<timestamp>`
4. All subsequent operations use directory mode automatically
### New exports:
- `migrateSessionStoreToDirectory(storePath)` — trigger migration
- `resolveSessionStoreDir(storePath)` — get directory path
- `sanitizeSessionKey(key)` / `desanitizeSessionKey(dirName)` — key ↔ dirname conversion
## Tests
Added `store.directory.test.ts` with 395 lines covering:
- Key sanitization round-trips
- Directory store path resolution
- Migration from JSON to directory (including backup verification)
- Load/save/update in directory mode
- Diff-based writes (only changed entries written)
- Cache invalidation for directory stores
- Maintenance (pruning, capping) in directory mode
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Migrates session storage from monolithic JSON to directory-per-session layout, addressing O(n) bottleneck with 80MB+ files. Implementation adds auto-detection, diff-based writes, per-entry I/O, atomic operations, and backward-compatible migration.
**Key changes:**
- New directory layout (`sessions.d/`) with per-session `meta.json` files (~200 bytes each)
- Diff-based writes in `updateSessionStore()` - only changed/removed entries are written
- `readSessionUpdatedAt()` now reads single file instead of parsing entire store
- Cache invalidation properly handles directory stores via explicit clearing on writes
- Migration function `migrateSessionStoreToDirectory()` with automatic backup
- Comprehensive test coverage (395 lines) for migration, CRUD ops, concurrent writes, and cache behavior
**Technical review:**
- Atomic writes use temp-file + rename pattern correctly
- Lock-based serialization prevents concurrent write conflicts
- Cache is explicitly invalidated on writes, avoiding stale data
- Directory mtime caching is safe due to explicit invalidation
- Key sanitization (`:` → `--`) works for current session key patterns but could collide if keys naturally contained `--` (not present in codebase)
- One documentation mismatch found (comment claims percent-encoding, implementation doesn't)
<h3>Confidence Score: 4/5</h3>
- Safe to merge - well-designed performance optimization with comprehensive tests and backward compatibility
- Implementation is sound with proper atomicity, locking, and cache management. The only issue found is a minor documentation mismatch. The change is backward compatible and includes thorough test coverage. Performance improvement is significant (O(n) → O(1) for single-session operations) and addresses a real production bottleneck.
- No files require special attention - both implementation and tests are well-structured
<sub>Last reviewed commit: bc69f6b</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#4664: fix: per-session metadata files to eliminate lock contention
by tsukhani · 2026-01-30
80.3%
#16061: fix(sessions): tolerate invalid sessionFile metadata
by haoyifan · 2026-02-14
74.1%
#16542: fix(sessions): use atomic temp+rename write on Windows
by aldoeliacim · 2026-02-14
73.3%
#15888: fix: store relative session file paths instead of absolute
by devAnon89 · 2026-02-14
72.0%
#6653: fix: persist archived session entry on /new or /reset
by leicao-me · 2026-02-01
71.5%
#15793: fix(sessions): gracefully handle stale cross-agent session file paths
by lxcong · 2026-02-13
71.4%
#17132: fix: filter out invalid session entries with empty sessionFile
by Limitless2023 · 2026-02-15
71.2%
#18179: CLI: add sessions --json-debug diagnostics
by p6l-richard · 2026-02-16
71.2%
#3410: fix(sessions): always compute session paths from current environment
by sakunsylvi · 2026-01-28
71.2%
#12884: Feature/named persistent sessions
by dylanb · 2026-02-09
70.8%