35aaeece6d
Per Codex: upsert_sources() wrote `active` but not `status`, so a candidate promoted inactive (the pipeline default) became active=0 + status='active' — the exact mirror drift Phase 1 set out to avoid (scheduler won't poll, admin UI shows "active"). Now derive status from an explicit value or from active, mirror active off status, and write both columns together (insert + conflict update). Test: promote_candidate(active=False) → status='paused', active=0. Also fix stale source_health docstring (now includes retired). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>