fix: fold session manager blockers into one candidate

This commit is contained in:
Affaan Mustafa
2026-03-24 23:08:27 -04:00
parent 7726c25e46
commit 1d0aa5ac2a
30 changed files with 1126 additions and 288 deletions

View File

@@ -2,7 +2,8 @@
* Session Manager Library for Claude Code
* Provides core session CRUD operations for listing, loading, and managing sessions
*
* Sessions are stored as markdown files in ~/.claude/sessions/ with format:
* Sessions are stored as markdown files in ~/.claude/session-data/ with
* legacy read compatibility for ~/.claude/sessions/:
* - YYYY-MM-DD-session.tmp (old format)
* - YYYY-MM-DD-<short-id>-session.tmp (new format)
*/
@@ -12,6 +13,7 @@ const path = require('path');
const {
getSessionsDir,
getSessionSearchDirs,
readFile,
log
} = require('./utils');
@@ -30,6 +32,7 @@ const SESSION_FILENAME_REGEX = /^(\d{4}-\d{2}-\d{2})(?:-([a-zA-Z0-9_][a-zA-Z0-9_
* @returns {object|null} Parsed metadata or null if invalid
*/
function parseSessionFilename(filename) {
if (!filename || typeof filename !== 'string') return null;
const match = filename.match(SESSION_FILENAME_REGEX);
if (!match) return null;
@@ -66,6 +69,72 @@ function getSessionPath(filename) {
return path.join(getSessionsDir(), filename);
}
function getSessionCandidates(options = {}) {
const {
date = null,
search = null
} = options;
const candidates = [];
for (const sessionsDir of getSessionSearchDirs()) {
if (!fs.existsSync(sessionsDir)) {
continue;
}
let entries;
try {
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
const filename = entry.name;
const metadata = parseSessionFilename(filename);
if (!metadata) continue;
if (date && metadata.date !== date) continue;
if (search && !metadata.shortId.includes(search)) continue;
const sessionPath = path.join(sessionsDir, filename);
let stats;
try {
stats = fs.statSync(sessionPath);
} catch {
continue;
}
candidates.push({
...metadata,
sessionPath,
hasContent: stats.size > 0,
size: stats.size,
modifiedTime: stats.mtime,
createdTime: stats.birthtime || stats.ctime
});
}
}
candidates.sort((a, b) => b.modifiedTime - a.modifiedTime);
const deduped = [];
const seenFilenames = new Set();
for (const session of candidates) {
if (seenFilenames.has(session.filename)) {
continue;
}
seenFilenames.add(session.filename);
deduped.push(session);
}
return deduped;
}
/**
* Read and parse session markdown content
* @param {string} sessionPath - Full path to session file
@@ -228,58 +297,12 @@ function getAllSessions(options = {}) {
const limitNum = Number(rawLimit);
const limit = Number.isNaN(limitNum) ? 50 : Math.max(1, Math.floor(limitNum));
const sessionsDir = getSessionsDir();
const sessions = getSessionCandidates({ date, search });
if (!fs.existsSync(sessionsDir)) {
if (sessions.length === 0) {
return { sessions: [], total: 0, offset, limit, hasMore: false };
}
const entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
const sessions = [];
for (const entry of entries) {
// Skip non-files (only process .tmp files)
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
const filename = entry.name;
const metadata = parseSessionFilename(filename);
if (!metadata) continue;
// Apply date filter
if (date && metadata.date !== date) {
continue;
}
// Apply search filter (search in short ID)
if (search && !metadata.shortId.includes(search)) {
continue;
}
const sessionPath = path.join(sessionsDir, filename);
// Get file stats (wrapped in try-catch to handle TOCTOU race where
// file is deleted between readdirSync and statSync)
let stats;
try {
stats = fs.statSync(sessionPath);
} catch {
continue; // File was deleted between readdir and stat
}
sessions.push({
...metadata,
sessionPath,
hasContent: stats.size > 0,
size: stats.size,
modifiedTime: stats.mtime,
createdTime: stats.birthtime || stats.ctime
});
}
// Sort by modified time (newest first)
sessions.sort((a, b) => b.modifiedTime - a.modifiedTime);
// Apply pagination
const paginatedSessions = sessions.slice(offset, offset + limit);
@@ -299,21 +322,16 @@ function getAllSessions(options = {}) {
* @returns {object|null} Session object or null if not found
*/
function getSessionById(sessionId, includeContent = false) {
const sessionsDir = getSessionsDir();
const sessions = getSessionCandidates();
if (!fs.existsSync(sessionsDir)) {
return null;
}
const entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
const filename = entry.name;
const metadata = parseSessionFilename(filename);
if (!metadata) continue;
for (const session of sessions) {
const filename = session.filename;
const metadata = {
filename: session.filename,
shortId: session.shortId,
date: session.date,
datetime: session.datetime
};
// Check if session ID matches (short ID or full filename without .tmp)
const shortIdMatch = sessionId.length > 0 && metadata.shortId !== 'no-id' && metadata.shortId.startsWith(sessionId);
@@ -324,30 +342,16 @@ function getSessionById(sessionId, includeContent = false) {
continue;
}
const sessionPath = path.join(sessionsDir, filename);
let stats;
try {
stats = fs.statSync(sessionPath);
} catch {
return null; // File was deleted between readdir and stat
}
const session = {
...metadata,
sessionPath,
size: stats.size,
modifiedTime: stats.mtime,
createdTime: stats.birthtime || stats.ctime
};
const sessionRecord = { ...session };
if (includeContent) {
session.content = getSessionContent(sessionPath);
session.metadata = parseSessionMetadata(session.content);
sessionRecord.content = getSessionContent(sessionRecord.sessionPath);
sessionRecord.metadata = parseSessionMetadata(sessionRecord.content);
// Pass pre-read content to avoid a redundant disk read
session.stats = getSessionStats(session.content || '');
sessionRecord.stats = getSessionStats(sessionRecord.content || '');
}
return session;
return sessionRecord;
}
return null;