feat(CF-580): Implement transcript-based session recovery for MCP
Add direct transcript ingestion and orphan recovery using Claude Code's JSONL transcripts instead of relying on daemon-based note synchronization. Changes: 1. **Database migration** (027_session_transcript_storage.sql): - Add transcript_jsonl, transcript_ingested_at, transcript_file_path columns - Add indexes for efficient ingestion tracking 2. **Transcript parser utility** (src/utils/transcript-parser.ts): - parseTranscriptFile(): Parse JSONL line-by-line, handle corrupt lines - encodeWorkingDir(): Convert paths to Claude Code directory encoding - findOrphanedTranscripts(): Scan for stale transcript files - ingestTranscriptToDatabase(): Main ingestion function for Node.js 3. **Orphan recovery enhancement** (src/tools/sessions.ts): - sessionRecoverOrphaned() now tries transcript ingestion first - Finds most recently modified JSONL in project directory - Falls back to legacy notes.md recovery for backward compatibility - Properly handles path encoding (/ and . → -) Benefits: - No daemon needed for recovery (Phase 2 will remove LaunchAgent) - Full transcript audit trail stored in database - Immediate recovery capability for orphaned sessions - Cleaner architecture (no markdown parsing complexity) - Compatible with Claude Code's UUID-based session files Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -488,8 +488,55 @@ export async function sessionRecoverOrphaned(args: { project?: string }): Promis
|
||||
`✓ Session ${session.project} #${session.session_number} marked as abandoned`
|
||||
);
|
||||
|
||||
// Attempt to recover notes from temp file
|
||||
// Attempt to recover transcript first (CF-580)
|
||||
let transcriptRecovered = false;
|
||||
if (session.working_directory) {
|
||||
// Construct projects path: ~/.claude/projects/{encoded-dir}/
|
||||
// Encoding: / and . → - (Claude Code removes dots from usernames)
|
||||
const home = process.env.HOME || '';
|
||||
const encodedDir = session.working_directory.replace(/[/\.]/g, '-');
|
||||
const projectsDir = `${home}/.claude/projects/${encodedDir}`;
|
||||
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
|
||||
if (fs.default.existsSync(projectsDir)) {
|
||||
// Find the most recently modified JSONL file
|
||||
const files = fs.default.readdirSync(projectsDir);
|
||||
const jsonlFiles = files
|
||||
.filter(f => f.endsWith('.jsonl'))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: `${projectsDir}/${f}`,
|
||||
mtime: fs.default.statSync(`${projectsDir}/${f}`).mtimeMs,
|
||||
}))
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
if (jsonlFiles.length > 0) {
|
||||
const latestFile = jsonlFiles[0];
|
||||
const transcriptContent = fs.default.readFileSync(latestFile.path, 'utf-8');
|
||||
const lineCount = transcriptContent.split('\n').filter(l => l.trim()).length;
|
||||
|
||||
// Update session with transcript
|
||||
await execute(
|
||||
`UPDATE sessions
|
||||
SET transcript_jsonl = $1, transcript_ingested_at = NOW(), transcript_file_path = $2, updated_at = NOW()
|
||||
WHERE id = $3`,
|
||||
[transcriptContent, latestFile.path, session.id]
|
||||
);
|
||||
|
||||
results.push(` → Recovered transcript (${lineCount} lines)`);
|
||||
transcriptRecovered = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip transcript recovery errors
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Attempt to recover notes from temp file if transcript not recovered
|
||||
if (!transcriptRecovered && session.working_directory) {
|
||||
const tempFilePath = `${session.working_directory}/.claude-session/${session.id}/notes.md`;
|
||||
|
||||
try {
|
||||
@@ -497,7 +544,7 @@ export async function sessionRecoverOrphaned(args: { project?: string }): Promis
|
||||
const recovered = await recoverNotesFromTempFile(session.id, tempFilePath, 'recovered');
|
||||
|
||||
if (recovered > 0) {
|
||||
results.push(` → Recovered ${recovered} note(s) from temp file`);
|
||||
results.push(` → Recovered ${recovered} note(s) from temp file (legacy)`);
|
||||
totalNotesRecovered += recovered;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user