feat(CF-580): Add schema support and metadata tracking for session notes recovery
- Migration 025: Add 'abandoned' status to sessions CHECK constraint (fixes blocking issue) - Migration 026: Add recovery metadata columns (recovered_from, recovered_at) to track note recovery source - Update sessionRecoverOrphaned to recover notes from temp files when marking sessions abandoned - Update notes-parser to track recovery source and timestamp for analytics These changes complete Priority 3, 5 and part of Priority 1 for CF-572 Session Notes Loss fix. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
13
migrations/025_add_abandoned_status.sql
Normal file
13
migrations/025_add_abandoned_status.sql
Normal file
@@ -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)';
|
||||||
16
migrations/026_session_notes_recovery_metadata.sql
Normal file
16
migrations/026_session_notes_recovery_metadata.sql
Normal file
@@ -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)';
|
||||||
@@ -487,12 +487,29 @@ export async function sessionRecoverOrphaned(args: { project?: string }): Promis
|
|||||||
results.push(
|
results.push(
|
||||||
`✓ Session ${session.project} #${session.session_number} marked as abandoned`
|
`✓ 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) {
|
} catch (err) {
|
||||||
results.push(`✗ Failed to mark session ${session.id} as abandoned: ${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')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ export async function recoverNotesFromTempFile(
|
|||||||
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
|
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
|
||||||
|
|
||||||
await execute(
|
await execute(
|
||||||
`INSERT INTO session_notes (session_id, note_type, content, embedding, created_at)
|
`INSERT INTO session_notes (session_id, note_type, content, embedding, created_at, recovered_from, recovered_at)
|
||||||
VALUES ($1, $2, $3, $4, NOW())
|
VALUES ($1, $2, $3, $4, NOW(), $5, NOW())
|
||||||
ON CONFLICT DO NOTHING`,
|
ON CONFLICT DO NOTHING`,
|
||||||
[sessionId, note.note_type, note.content, embeddingValue]
|
[sessionId, note.note_type, note.content, embeddingValue, source]
|
||||||
);
|
);
|
||||||
|
|
||||||
insertedCount++;
|
insertedCount++;
|
||||||
|
|||||||
Reference in New Issue
Block a user