diff --git a/migrations/025_add_abandoned_status.sql b/migrations/025_add_abandoned_status.sql new file mode 100644 index 0000000..ba62135 --- /dev/null +++ b/migrations/025_add_abandoned_status.sql @@ -0,0 +1,13 @@ +-- Migration 025: Add 'abandoned' status to sessions +-- Purpose: Fix CF-572 schema constraint issue preventing orphan recovery +-- Context: sessionRecoverOrphaned() tries to set status='abandoned' but constraint only allows 'active', 'completed', 'interrupted' + +ALTER TABLE sessions + DROP CONSTRAINT IF EXISTS sessions_status_check; + +ALTER TABLE sessions + ADD CONSTRAINT sessions_status_check + CHECK (status IN ('active', 'completed', 'interrupted', 'abandoned')); + +COMMENT ON CONSTRAINT sessions_status_check ON sessions + IS 'Valid session statuses including abandoned for orphaned sessions (CF-572)'; diff --git a/migrations/026_session_notes_recovery_metadata.sql b/migrations/026_session_notes_recovery_metadata.sql new file mode 100644 index 0000000..1a529f8 --- /dev/null +++ b/migrations/026_session_notes_recovery_metadata.sql @@ -0,0 +1,16 @@ +-- Migration 026: Add recovery metadata to session_notes +-- Purpose: Track which notes were recovered vs normally saved for CF-572 analytics + +ALTER TABLE session_notes + ADD COLUMN IF NOT EXISTS recovered_from TEXT, + ADD COLUMN IF NOT EXISTS recovered_at TIMESTAMP WITH TIME ZONE; + +CREATE INDEX IF NOT EXISTS idx_session_notes_recovered + ON session_notes(recovered_from) + WHERE recovered_from IS NOT NULL; + +COMMENT ON COLUMN session_notes.recovered_from + IS 'Source of recovery: "recovered" (daemon/orphan), "manual" (MCP tool), NULL (normal save)'; + +COMMENT ON COLUMN session_notes.recovered_at + IS 'Timestamp when note was recovered (NULL for normal saves)'; diff --git a/src/tools/sessions.ts b/src/tools/sessions.ts index 51442e5..c0facc9 100644 --- a/src/tools/sessions.ts +++ b/src/tools/sessions.ts @@ -487,12 +487,29 @@ export async function sessionRecoverOrphaned(args: { project?: string }): Promis results.push( `✓ Session ${session.project} #${session.session_number} marked as abandoned` ); + + // Attempt to recover notes from temp file + if (session.working_directory) { + const tempFilePath = `${session.working_directory}/.claude-session/${session.id}/notes.md`; + + try { + const { recoverNotesFromTempFile } = await import('../utils/notes-parser.js'); + const recovered = await recoverNotesFromTempFile(session.id, tempFilePath, 'recovered'); + + if (recovered > 0) { + results.push(` → Recovered ${recovered} note(s) from temp file`); + totalNotesRecovered += recovered; + } + } catch (err) { + results.push(` ⚠ Could not recover notes: ${err instanceof Error ? err.message : String(err)}`); + } + } } catch (err) { results.push(`✗ Failed to mark session ${session.id} as abandoned: ${err}`); } } - return `Recovered ${orphanedSessions.length} orphaned session(s):\n${results.join('\n')}`; + return `Recovered ${orphanedSessions.length} orphaned session(s), ${totalNotesRecovered} notes:\n${results.join('\n')}`; } /** diff --git a/src/utils/notes-parser.ts b/src/utils/notes-parser.ts index 5fe609e..6e7b9ba 100644 --- a/src/utils/notes-parser.ts +++ b/src/utils/notes-parser.ts @@ -83,10 +83,10 @@ export async function recoverNotesFromTempFile( const embeddingValue = embedding ? formatEmbedding(embedding) : null; await execute( - `INSERT INTO session_notes (session_id, note_type, content, embedding, created_at) - VALUES ($1, $2, $3, $4, NOW()) + `INSERT INTO session_notes (session_id, note_type, content, embedding, created_at, recovered_from, recovered_at) + VALUES ($1, $2, $3, $4, NOW(), $5, NOW()) ON CONFLICT DO NOTHING`, - [sessionId, note.note_type, note.content, embeddingValue] + [sessionId, note.note_type, note.content, embeddingValue, source] ); insertedCount++;