--- id: "bug-337" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "controller" severity: "unknown" timestamp: "2026-06-09T00:02:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/controller/ha.c"] tags: ["ha", "heartbeat", "liveness", "vote-gate", "dead-field", "gap-analysis"] related: [] legacy: true --- # bug-337: Vote-deny gate inert: ha_heartbeat_is_fresh reads ha->last_heartbeat which has no production… ## Description Vote-deny gate inert: ha_heartbeat_is_fresh reads ha->last_heartbeat which has no production writer after the liveness refactor ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause ha-heartbeat-liveness refactor moved activity tracking to ha->last_activity but ha_heartbeat_is_fresh (ha.c:1762) still reads the orphaned last_heartbeat timespec; only tests write it, so freshness is always stale after ~heartbeat_timeout_ms of uptime and standbys grant votes despite a live primary (gates at ha.c:214 and ha.c:1841) ## Fix FIXED 2026-06-09: ha_heartbeat_is_fresh now reads ha->last_activity (returns 0 when last_activity==0); dead last_heartbeat field removed from ha_state_t; test_ha_vote_gate.c helper rewritten as set_last_activity_age and fresh/stale tests now also set primary_ctrl_fd. All tests green. ## Verification _Not recorded (migrated from buglog.json)._ ## References _Not recorded (migrated from buglog.json)._