d2e733e636
Reported in Discord by @spherohero: `sessions recover --allow-partial` copied 20,817 of 20,824 message rows, then orphan cleanup deleted every one of them because no session row was salvageable. Final output: 0 sessions, 0 messages. The salvage worked and then threw the result away -- the exact opposite of what --allow-partial exists to do. _cleanup_partial_orphans() removed dependent rows whose session_id had no matching sessions row. That is correct when a few sessions are lost; it is catastrophic when the sessions b-tree is damaged worse than messages, which is the common shape (sessions is small and hot, messages is large). Reproduced exactly: 500 readable messages, unreadable sessions -> 500 copied, 500 removed, empty output. Now _reconstruct_missing_sessions() runs FIRST, inside the same transaction, synthesizing a minimal row per orphaned session_id (only id/source/started_at are NOT NULL). started_at comes from the earliest surviving message. Placeholders carry source='recovered' and an explicit title so a fabricated session can never be mistaken for an original. Same repro now retains all 500 messages under 1 reconstructed session. Reconstruction is reported as LOSS, not a clean recovery: session metadata (title, model, timestamps, cost) is genuinely gone even though the conversation text survived. loss_detected=True, partial=True, complete=False, with a warning naming the counts. Also fixes total_removed_or_relinked, which summed every dict value and would now have counted retained messages as removed. Sabotage-verified: removing the reconstruction call restores the wipe and fails the test. 942 targeted tests green.