Files
CHORUS/docs/comprehensive/packages/election.md
anthonyrawlins c5b7311a8b docs: Add Phase 3 coordination and infrastructure documentation
Comprehensive documentation for coordination, messaging, discovery, and internal systems.

Core Coordination Packages:
- pkg/election - Democratic leader election (uptime-based, heartbeat mechanism, SLURP integration)
- pkg/coordination - Meta-coordination with dependency detection (4 built-in rules)
- coordinator/ - Task orchestration and assignment (AI-powered scoring)
- discovery/ - mDNS peer discovery (automatic LAN detection)

Messaging & P2P Infrastructure:
- pubsub/ - GossipSub messaging (31 message types, role-based topics, HMMM integration)
- p2p/ - libp2p networking (DHT modes, connection management, security)

Monitoring & Health:
- pkg/metrics - Prometheus metrics (80+ metrics across 12 categories)
- pkg/health - Health monitoring (4 HTTP endpoints, enhanced checks, graceful degradation)

Internal Systems:
- internal/licensing - License validation (KACHING integration, cluster leases, fail-closed)
- internal/hapui - Human Agent Portal UI (9 commands, HMMM wizard, UCXL browser, decision voting)
- internal/backbeat - P2P operation telemetry (6 phases, beat synchronization, health reporting)

Documentation Statistics (Phase 3):
- 10 packages documented (~18,000 lines)
- 31 PubSub message types cataloged
- 80+ Prometheus metrics documented
- Complete API references with examples
- Integration patterns and best practices

Key Features Documented:
- Election: 5 triggers, candidate scoring (5 weighted components), stability windows
- Coordination: AI-powered dependency detection, cross-repo sessions, escalation handling
- PubSub: Topic patterns, message envelopes, SHHH redaction, Hypercore logging
- Metrics: All metric types with labels, Prometheus scrape config, alert rules
- Health: Liveness vs readiness, critical checks, Kubernetes integration
- Licensing: Grace periods, circuit breaker, cluster lease management
- HAP UI: Interactive terminal commands, HMMM composition wizard, web interface (beta)
- BACKBEAT: 6-phase operation tracking, beat budget estimation, drift detection

Implementation Status Marked:
-  Production: Election, metrics, health, licensing, pubsub, p2p, discovery, coordinator
- 🔶 Beta: HAP web interface, BACKBEAT telemetry, advanced coordination
- 🔷 Alpha: SLURP election scoring
- ⚠️ Experimental: Meta-coordination, AI-powered dependency detection

Progress: 22/62 files complete (35%)

Next Phase: AI providers, SLURP system, API layer, reasoning engine

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 18:27:39 +10:00

2757 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CHORUS Election Package Documentation
**Package:** `chorus/pkg/election`
**Purpose:** Democratic leader election and consensus coordination for distributed CHORUS agents
**Status:** Production-ready core system; SLURP integration experimental
---
## Table of Contents
1. [Overview](#overview)
2. [Architecture](#architecture)
3. [Election Algorithm](#election-algorithm)
4. [Admin Heartbeat Mechanism](#admin-heartbeat-mechanism)
5. [Election Triggers](#election-triggers)
6. [Candidate Scoring System](#candidate-scoring-system)
7. [SLURP Integration](#slurp-integration)
8. [Quorum and Consensus](#quorum-and-consensus)
9. [API Reference](#api-reference)
10. [Configuration](#configuration)
11. [Message Formats](#message-formats)
12. [State Machine](#state-machine)
13. [Callbacks and Events](#callbacks-and-events)
14. [Testing](#testing)
15. [Production Considerations](#production-considerations)
---
## Overview
The election package implements a democratic leader election system for distributed CHORUS agents. It enables autonomous agents to elect an "admin" node responsible for coordination, context curation, and key management tasks. The system uses uptime-based voting, capability scoring, and heartbeat monitoring to maintain stable leadership while allowing graceful failover.
### Key Features
- **Democratic Election**: Nodes vote for the most qualified candidate based on uptime, capabilities, and resources
- **Heartbeat Monitoring**: Active admin sends periodic heartbeats (5s interval) to prove liveness
- **Automatic Failover**: Elections triggered on heartbeat timeout (15s), split-brain detection, or manual triggers
- **Capability-Based Scoring**: Candidates scored on admin capabilities, resources, uptime, and experience
- **SLURP Integration**: Experimental context leadership with Project Manager intelligence capabilities
- **Stability Windows**: Prevents election churn with configurable minimum term durations
- **Graceful Transitions**: Callback system for clean leadership handoffs
### Use Cases
1. **Admin Node Selection**: Elect a coordinator for project-wide context curation
2. **Split-Brain Recovery**: Resolve network partition conflicts through re-election
3. **Load Distribution**: Select admin based on available resources and current load
4. **Failover**: Automatic promotion of standby nodes when admin becomes unavailable
5. **Context Leadership**: (SLURP) Specialized election for AI context generation leadership
---
## Architecture
### Component Structure
```
election/
├── election.go # Core election manager (production)
├── interfaces.go # Shared type definitions
├── slurp_election.go # SLURP election interface (experimental)
├── slurp_manager.go # SLURP election manager implementation (experimental)
├── slurp_scoring.go # SLURP candidate scoring (experimental)
└── election_test.go # Unit tests
```
### Core Components
#### 1. ElectionManager (Production)
The `ElectionManager` is the production-ready core election coordinator:
```go
type ElectionManager struct {
ctx context.Context
cancel context.CancelFunc
config *config.Config
host libp2p.Host
pubsub *pubsub.PubSub
nodeID string
// Election state
mu sync.RWMutex
state ElectionState
currentTerm int
lastHeartbeat time.Time
currentAdmin string
candidates map[string]*AdminCandidate
votes map[string]string // voter -> candidate
// Timers and channels
heartbeatTimer *time.Timer
discoveryTimer *time.Timer
electionTimer *time.Timer
electionTrigger chan ElectionTrigger
// Heartbeat management
heartbeatManager *HeartbeatManager
// Callbacks
onAdminChanged func(oldAdmin, newAdmin string)
onElectionComplete func(winner string)
// Stability windows (prevents election churn)
lastElectionTime time.Time
electionStabilityWindow time.Duration
leaderStabilityWindow time.Duration
startTime time.Time
}
```
**Key Responsibilities:**
- Discovery of existing admin via broadcast queries
- Triggering elections based on heartbeat timeouts or manual triggers
- Managing candidate announcements and vote collection
- Determining election winners based on votes and scores
- Broadcasting election results to cluster
- Managing admin heartbeat lifecycle
#### 2. HeartbeatManager
Manages the admin heartbeat transmission lifecycle:
```go
type HeartbeatManager struct {
mu sync.Mutex
isRunning bool
stopCh chan struct{}
ticker *time.Ticker
electionMgr *ElectionManager
logger func(msg string, args ...interface{})
}
```
**Configuration:**
- **Heartbeat Interval**: `HeartbeatTimeout / 2` (default ~7.5s)
- **Heartbeat Timeout**: 15 seconds (configurable via `Security.ElectionConfig.HeartbeatTimeout`)
- **Transmission**: Only when node is current admin
- **Lifecycle**: Automatically started/stopped on leadership changes
#### 3. SLURPElectionManager (Experimental)
Extends `ElectionManager` with SLURP contextual intelligence for Project Manager duties:
```go
type SLURPElectionManager struct {
*ElectionManager // Embeds base election manager
// SLURP-specific state
contextMu sync.RWMutex
contextManager ContextManager
slurpConfig *SLURPElectionConfig
contextCallbacks *ContextLeadershipCallbacks
// Context leadership state
isContextLeader bool
contextTerm int64
contextStartedAt *time.Time
lastHealthCheck time.Time
// Failover state
failoverState *ContextFailoverState
transferInProgress bool
// Monitoring
healthMonitor *ContextHealthMonitor
metricsCollector *ContextMetricsCollector
// Shutdown coordination
contextShutdown chan struct{}
contextWg sync.WaitGroup
}
```
**Additional Capabilities:**
- Context generation leadership
- Graceful leadership transfer with state preservation
- Health monitoring and metrics collection
- Failover state validation and recovery
- Advanced scoring for AI capabilities
---
## Election Algorithm
### Democratic Election Process
The election system implements a **democratic voting algorithm** where nodes elect the most qualified candidate based on objective metrics.
#### Election Flow
```
┌─────────────────────────────────────────────────────────────┐
│ 1. DISCOVERY PHASE │
│ - Node broadcasts admin discovery request │
│ - Existing admin (if any) responds │
│ - Node updates currentAdmin if discovered │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. ELECTION TRIGGER │
│ - Heartbeat timeout (15s without admin heartbeat) │
│ - No admin discovered after discovery attempts │
│ - Split-brain detection │
│ - Manual trigger │
│ - Quorum restoration │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. CANDIDATE ANNOUNCEMENT │
│ - Eligible nodes announce candidacy │
│ - Include: NodeID, capabilities, uptime, resources │
│ - Calculate and include candidate score │
│ - Broadcast to election topic │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. VOTE COLLECTION (Election Timeout Period) │
│ - Nodes receive candidate announcements │
│ - Nodes cast votes for highest-scoring candidate │
│ - Votes broadcast to cluster │
│ - Duration: Security.ElectionConfig.ElectionTimeout │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. WINNER DETERMINATION │
│ - Tally votes for each candidate │
│ - Winner: Most votes (ties broken by score) │
│ - Fallback: Highest score if no votes cast │
│ - Broadcast election winner │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6. LEADERSHIP TRANSITION │
│ - Update currentAdmin │
│ - Winner starts admin heartbeat │
│ - Previous admin stops heartbeat (if different node) │
│ - Trigger callbacks (OnAdminChanged, OnElectionComplete) │
│ - Return to DISCOVERY/MONITORING phase │
└─────────────────────────────────────────────────────────────┘
```
### Eligibility Criteria
A node can become admin if it has **any** of these capabilities:
- `admin_election` - Core admin election capability
- `context_curation` - Context management capability
- `project_manager` - Project coordination capability
Checked via `ElectionManager.canBeAdmin()`:
```go
func (em *ElectionManager) canBeAdmin() bool {
for _, cap := range em.config.Agent.Capabilities {
if cap == "admin_election" || cap == "context_curation" || cap == "project_manager" {
return true
}
}
return false
}
```
### Election Timing
- **Discovery Loop**: Runs continuously, interval = `Security.ElectionConfig.DiscoveryTimeout` (default: 10s)
- **Election Timeout**: `Security.ElectionConfig.ElectionTimeout` (default: 30s)
- **Randomized Delay**: When triggering election after discovery failure, adds random delay (`DiscoveryTimeout` to `2×DiscoveryTimeout`) to prevent simultaneous elections
---
## Admin Heartbeat Mechanism
The admin heartbeat proves liveness and prevents unnecessary elections.
### Heartbeat Configuration
| Parameter | Value | Description |
|-----------|-------|-------------|
| **Interval** | `HeartbeatTimeout / 2` | Heartbeat transmission frequency (~7.5s) |
| **Timeout** | `HeartbeatTimeout` | Max time without heartbeat before election (15s) |
| **Topic** | `CHORUS/admin/heartbeat/v1` | PubSub topic for heartbeats |
| **Format** | JSON | Message serialization format |
### Heartbeat Message Format
```json
{
"node_id": "QmXxx...abc",
"timestamp": "2025-09-30T18:15:30.123456789Z"
}
```
**Fields:**
- `node_id` (string): Admin node's ID
- `timestamp` (RFC3339Nano): When heartbeat was sent
### Heartbeat Lifecycle
#### Starting Heartbeat (Becoming Admin)
```go
// Automatically called when node becomes admin
func (hm *HeartbeatManager) StartHeartbeat() error {
hm.mu.Lock()
defer hm.mu.Unlock()
if hm.isRunning {
return nil // Already running
}
if !hm.electionMgr.IsCurrentAdmin() {
return fmt.Errorf("not admin, cannot start heartbeat")
}
hm.stopCh = make(chan struct{})
interval := hm.electionMgr.config.Security.ElectionConfig.HeartbeatTimeout / 2
hm.ticker = time.NewTicker(interval)
hm.isRunning = true
go hm.heartbeatLoop()
return nil
}
```
#### Stopping Heartbeat (Losing Admin)
```go
// Automatically called when node loses admin role
func (hm *HeartbeatManager) StopHeartbeat() error {
hm.mu.Lock()
defer hm.mu.Unlock()
if !hm.isRunning {
return nil // Already stopped
}
close(hm.stopCh)
if hm.ticker != nil {
hm.ticker.Stop()
hm.ticker = nil
}
hm.isRunning = false
return nil
}
```
#### Heartbeat Transmission Loop
```go
func (hm *HeartbeatManager) heartbeatLoop() {
defer func() {
hm.mu.Lock()
hm.isRunning = false
hm.mu.Unlock()
}()
for {
select {
case <-hm.ticker.C:
// Only send heartbeat if still admin
if hm.electionMgr.IsCurrentAdmin() {
if err := hm.electionMgr.SendAdminHeartbeat(); err != nil {
hm.logger("Failed to send heartbeat: %v", err)
}
} else {
hm.logger("No longer admin, stopping heartbeat")
return
}
case <-hm.stopCh:
return
case <-hm.electionMgr.ctx.Done():
return
}
}
}
```
### Heartbeat Processing
When a node receives a heartbeat:
```go
func (em *ElectionManager) handleAdminHeartbeat(data []byte) {
var heartbeat struct {
NodeID string `json:"node_id"`
Timestamp time.Time `json:"timestamp"`
}
if err := json.Unmarshal(data, &heartbeat); err != nil {
log.Printf("❌ Failed to unmarshal heartbeat: %v", err)
return
}
em.mu.Lock()
defer em.mu.Unlock()
// Update admin and heartbeat timestamp
if em.currentAdmin == "" || em.currentAdmin == heartbeat.NodeID {
em.currentAdmin = heartbeat.NodeID
em.lastHeartbeat = heartbeat.Timestamp
}
}
```
### Timeout Detection
Checked during discovery loop:
```go
func (em *ElectionManager) performAdminDiscovery() {
em.mu.Lock()
lastHeartbeat := em.lastHeartbeat
em.mu.Unlock()
// Check if admin heartbeat has timed out
if !lastHeartbeat.IsZero() &&
time.Since(lastHeartbeat) > em.config.Security.ElectionConfig.HeartbeatTimeout {
log.Printf("⚰️ Admin heartbeat timeout detected (last: %v)", lastHeartbeat)
em.TriggerElection(TriggerHeartbeatTimeout)
return
}
}
```
---
## Election Triggers
Elections can be triggered by multiple events, each with different stability guarantees.
### Trigger Types
```go
type ElectionTrigger string
const (
TriggerHeartbeatTimeout ElectionTrigger = "admin_heartbeat_timeout"
TriggerDiscoveryFailure ElectionTrigger = "no_admin_discovered"
TriggerSplitBrain ElectionTrigger = "split_brain_detected"
TriggerQuorumRestored ElectionTrigger = "quorum_restored"
TriggerManual ElectionTrigger = "manual_trigger"
)
```
### Trigger Details
#### 1. Heartbeat Timeout
**When:** No admin heartbeat received for `HeartbeatTimeout` duration (15s)
**Behavior:**
- Most common trigger for failover
- Indicates admin node failure or network partition
- Immediate election trigger (no stability window applies)
**Example:**
```go
if time.Since(lastHeartbeat) > em.config.Security.ElectionConfig.HeartbeatTimeout {
em.TriggerElection(TriggerHeartbeatTimeout)
}
```
#### 2. Discovery Failure
**When:** No admin discovered after multiple discovery attempts
**Behavior:**
- Occurs on cluster startup or after total admin loss
- Includes randomized delay to prevent simultaneous elections
- Base delay: `2 × DiscoveryTimeout` + random(`DiscoveryTimeout`)
**Example:**
```go
if currentAdmin == "" && em.canBeAdmin() {
baseDelay := em.config.Security.ElectionConfig.DiscoveryTimeout * 2
randomDelay := time.Duration(rand.Intn(int(em.config.Security.ElectionConfig.DiscoveryTimeout)))
totalDelay := baseDelay + randomDelay
time.Sleep(totalDelay)
if stillNoAdmin && stillIdle && em.canBeAdmin() {
em.TriggerElection(TriggerDiscoveryFailure)
}
}
```
#### 3. Split-Brain Detection
**When:** Multiple nodes believe they are admin
**Behavior:**
- Detected through conflicting admin announcements
- Forces re-election to resolve conflict
- Should be rare in properly configured clusters
**Usage:** (Implementation-specific, typically in cluster coordination layer)
#### 4. Quorum Restored
**When:** Network partition heals and quorum is re-established
**Behavior:**
- Allows cluster to re-elect with full member participation
- Ensures minority partition doesn't maintain stale admin
**Usage:** (Implementation-specific, typically in quorum management layer)
#### 5. Manual Trigger
**When:** Explicitly triggered via API or administrative action
**Behavior:**
- Used for planned leadership transfers
- Used for testing and debugging
- Respects stability windows (can be overridden)
**Example:**
```go
em.TriggerElection(TriggerManual)
```
### Stability Windows
To prevent election churn, the system enforces minimum durations between elections:
#### Election Stability Window
**Default:** `2 × DiscoveryTimeout` (20s)
**Configuration:** Environment variable `CHORUS_ELECTION_MIN_TERM`
Prevents rapid back-to-back elections regardless of admin state.
```go
func getElectionStabilityWindow(cfg *config.Config) time.Duration {
if stability := os.Getenv("CHORUS_ELECTION_MIN_TERM"); stability != "" {
if duration, err := time.ParseDuration(stability); err == nil {
return duration
}
}
if cfg.Security.ElectionConfig.DiscoveryTimeout > 0 {
return cfg.Security.ElectionConfig.DiscoveryTimeout * 2
}
return 30 * time.Second // Fallback
}
```
#### Leader Stability Window
**Default:** `3 × HeartbeatTimeout` (45s)
**Configuration:** Environment variable `CHORUS_LEADER_MIN_TERM`
Prevents challenging a healthy leader too quickly after election.
```go
func getLeaderStabilityWindow(cfg *config.Config) time.Duration {
if stability := os.Getenv("CHORUS_LEADER_MIN_TERM"); stability != "" {
if duration, err := time.ParseDuration(stability); err == nil {
return duration
}
}
if cfg.Security.ElectionConfig.HeartbeatTimeout > 0 {
return cfg.Security.ElectionConfig.HeartbeatTimeout * 3
}
return 45 * time.Second // Fallback
}
```
#### Stability Window Enforcement
```go
func (em *ElectionManager) TriggerElection(trigger ElectionTrigger) {
em.mu.RLock()
currentState := em.state
currentAdmin := em.currentAdmin
lastElection := em.lastElectionTime
em.mu.RUnlock()
if currentState != StateIdle {
log.Printf("🗳️ Election already in progress (state: %s), ignoring trigger: %s",
currentState, trigger)
return
}
now := time.Now()
if !lastElection.IsZero() {
timeSinceElection := now.Sub(lastElection)
// Leader stability window (if we have a current admin)
if currentAdmin != "" && timeSinceElection < em.leaderStabilityWindow {
log.Printf("⏳ Leader stability window active (%.1fs remaining), ignoring trigger: %s",
(em.leaderStabilityWindow - timeSinceElection).Seconds(), trigger)
return
}
// General election stability window
if timeSinceElection < em.electionStabilityWindow {
log.Printf("⏳ Election stability window active (%.1fs remaining), ignoring trigger: %s",
(em.electionStabilityWindow - timeSinceElection).Seconds(), trigger)
return
}
}
select {
case em.electionTrigger <- trigger:
log.Printf("🗳️ Election triggered: %s", trigger)
default:
log.Printf("⚠️ Election trigger buffer full, ignoring: %s", trigger)
}
}
```
**Key Points:**
- Stability windows prevent election storms during network instability
- Heartbeat timeout triggers bypass some stability checks (admin definitely unavailable)
- Manual triggers respect stability windows unless explicitly overridden
- Referenced in WHOOSH issue #7 as fix for election churn
---
## Candidate Scoring System
Candidates are scored on multiple dimensions to determine the most qualified admin.
### Base Election Scoring (Production)
#### Scoring Formula
```
finalScore = uptimeScore * 0.3 +
capabilityScore * 0.2 +
resourceScore * 0.2 +
networkQuality * 0.15 +
experienceScore * 0.15
```
#### Component Scores
**1. Uptime Score (Weight: 0.3)**
Measures node stability and continuous availability.
```go
uptimeScore := min(1.0, candidate.Uptime.Hours() / 24.0)
```
- **Calculation:** Linear scaling from 0 to 1.0 over 24 hours
- **Max Score:** 1.0 (achieved at 24+ hours uptime)
- **Purpose:** Prefer nodes with proven stability
**2. Capability Score (Weight: 0.2)**
Measures administrative and coordination capabilities.
```go
capabilityScore := 0.0
adminCapabilities := []string{
"admin_election",
"context_curation",
"key_reconstruction",
"semantic_analysis",
"project_manager",
}
for _, cap := range candidate.Capabilities {
for _, adminCap := range adminCapabilities {
if cap == adminCap {
weight := 0.25 // Default weight
// Project manager capabilities get higher weight
if adminCap == "project_manager" || adminCap == "context_curation" {
weight = 0.35
}
capabilityScore += weight
}
}
}
capabilityScore = min(1.0, capabilityScore)
```
- **Admin Capabilities:** +0.25 per capability (standard)
- **Premium Capabilities:** +0.35 for `project_manager` and `context_curation`
- **Max Score:** 1.0 (capped)
- **Purpose:** Prefer nodes with admin-specific capabilities
**3. Resource Score (Weight: 0.2)**
Measures available compute resources (lower usage = better).
```go
resourceScore := (1.0 - candidate.Resources.CPUUsage) * 0.3 +
(1.0 - candidate.Resources.MemoryUsage) * 0.3 +
(1.0 - candidate.Resources.DiskUsage) * 0.2 +
candidate.Resources.NetworkQuality * 0.2
```
- **CPU Usage:** Lower is better (30% weight)
- **Memory Usage:** Lower is better (30% weight)
- **Disk Usage:** Lower is better (20% weight)
- **Network Quality:** Higher is better (20% weight)
- **Purpose:** Prefer nodes with available resources
**4. Network Quality Score (Weight: 0.15)**
Direct measure of network connectivity quality.
```go
networkScore := candidate.Resources.NetworkQuality // Range: 0.0 to 1.0
```
- **Source:** Measured network quality metric
- **Range:** 0.0 (poor) to 1.0 (excellent)
- **Purpose:** Ensure admin has good connectivity
**5. Experience Score (Weight: 0.15)**
Measures long-term operational experience.
```go
experienceScore := min(1.0, candidate.Experience.Hours() / 168.0)
```
- **Calculation:** Linear scaling from 0 to 1.0 over 1 week (168 hours)
- **Max Score:** 1.0 (achieved at 1+ week experience)
- **Purpose:** Prefer nodes with proven long-term reliability
#### Resource Metrics Structure
```go
type ResourceMetrics struct {
CPUUsage float64 `json:"cpu_usage"` // 0.0 to 1.0 (0-100%)
MemoryUsage float64 `json:"memory_usage"` // 0.0 to 1.0 (0-100%)
DiskUsage float64 `json:"disk_usage"` // 0.0 to 1.0 (0-100%)
NetworkQuality float64 `json:"network_quality"` // 0.0 to 1.0 (quality score)
}
```
**Note:** Current implementation uses simulated values. Production systems should integrate actual resource monitoring.
### SLURP Candidate Scoring (Experimental)
SLURP extends base scoring with contextual intelligence metrics for Project Manager leadership.
#### Extended Scoring Formula
```
finalScore = baseScore * (baseWeightsSum) +
contextCapabilityScore * contextWeight +
intelligenceScore * intelligenceWeight +
coordinationScore * coordinationWeight +
qualityScore * qualityWeight +
performanceScore * performanceWeight +
specializationScore * specializationWeight +
availabilityScore * availabilityWeight +
reliabilityScore * reliabilityWeight
```
Normalized by total weight sum.
#### SLURP Scoring Weights (Default)
```go
func DefaultSLURPScoringWeights() *SLURPScoringWeights {
return &SLURPScoringWeights{
// Base election weights (total: 0.4)
UptimeWeight: 0.08,
CapabilityWeight: 0.10,
ResourceWeight: 0.08,
NetworkWeight: 0.06,
ExperienceWeight: 0.08,
// SLURP-specific weights (total: 0.6)
ContextCapabilityWeight: 0.15, // Most important for context leadership
IntelligenceWeight: 0.12,
CoordinationWeight: 0.10,
QualityWeight: 0.08,
PerformanceWeight: 0.06,
SpecializationWeight: 0.04,
AvailabilityWeight: 0.03,
ReliabilityWeight: 0.02,
}
}
```
#### SLURP Component Scores
**1. Context Capability Score (Weight: 0.15)**
Core context generation capabilities:
```go
score := 0.0
if caps.ContextGeneration { score += 0.3 } // Required for leadership
if caps.ContextCuration { score += 0.2 } // Content quality
if caps.ContextDistribution { score += 0.2 } // Delivery capability
if caps.ContextStorage { score += 0.1 } // Persistence
if caps.SemanticAnalysis { score += 0.1 } // Advanced analysis
if caps.RAGIntegration { score += 0.1 } // RAG capability
```
**2. Intelligence Score (Weight: 0.12)**
AI and analysis capabilities:
```go
score := 0.0
if caps.SemanticAnalysis { score += 0.25 }
if caps.RAGIntegration { score += 0.25 }
if caps.TemporalAnalysis { score += 0.25 }
if caps.DecisionTracking { score += 0.25 }
// Apply quality multiplier
score = score * caps.GenerationQuality
```
**3. Coordination Score (Weight: 0.10)**
Cluster management capabilities:
```go
score := 0.0
if caps.ClusterCoordination { score += 0.3 }
if caps.LoadBalancing { score += 0.25 }
if caps.HealthMonitoring { score += 0.2 }
if caps.ResourceManagement { score += 0.25 }
```
**4. Quality Score (Weight: 0.08)**
Average of quality metrics:
```go
score := (caps.GenerationQuality + caps.ProcessingSpeed + caps.AccuracyScore) / 3.0
```
**5. Performance Score (Weight: 0.06)**
Historical operation success:
```go
totalOps := caps.SuccessfulOperations + caps.FailedOperations
successRate := float64(caps.SuccessfulOperations) / float64(totalOps)
// Response time score (1s optimal, 10s poor)
responseTimeScore := calculateResponseTimeScore(caps.AverageResponseTime)
score := (successRate * 0.7) + (responseTimeScore * 0.3)
```
**6. Specialization Score (Weight: 0.04)**
Domain expertise coverage:
```go
domainCoverage := float64(len(caps.DomainExpertise)) / 10.0
domainCoverage = min(1.0, domainCoverage)
score := (caps.SpecializationScore * 0.6) + (domainCoverage * 0.4)
```
**7. Availability Score (Weight: 0.03)**
Resource availability:
```go
cpuScore := min(1.0, caps.AvailableCPU / 8.0) // 8 cores = 1.0
memoryScore := min(1.0, caps.AvailableMemory / 16GB) // 16GB = 1.0
storageScore := min(1.0, caps.AvailableStorage / 1TB) // 1TB = 1.0
networkScore := min(1.0, caps.NetworkBandwidth / 1Gbps) // 1Gbps = 1.0
score := (cpuScore * 0.3) + (memoryScore * 0.3) +
(storageScore * 0.2) + (networkScore * 0.2)
```
**8. Reliability Score (Weight: 0.02)**
Uptime and reliability:
```go
score := (caps.ReliabilityScore * 0.6) + (caps.UptimePercentage * 0.4)
```
#### SLURP Requirements Filtering
Candidates must meet minimum requirements to be eligible:
```go
func DefaultSLURPLeadershipRequirements() *SLURPLeadershipRequirements {
return &SLURPLeadershipRequirements{
RequiredCapabilities: []string{"context_generation", "context_curation"},
PreferredCapabilities: []string{"semantic_analysis", "cluster_coordination", "rag_integration"},
MinQualityScore: 0.6,
MinReliabilityScore: 0.7,
MinUptimePercentage: 0.8,
MinCPU: 2.0, // 2 CPU cores
MinMemory: 4 * GB, // 4GB
MinStorage: 100 * GB, // 100GB
MinNetworkBandwidth: 100 * Mbps, // 100 Mbps
MinSuccessfulOperations: 10,
MaxFailureRate: 0.1, // 10% max
MaxResponseTime: 5 * time.Second,
}
}
```
**Disqualification:** Candidates failing requirements receive score of 0.0 and are marked with disqualification reasons.
#### Score Adjustments (Bonuses/Penalties)
```go
// Bonuses
if caps.GenerationQuality > 0.95 {
finalScore += 0.05 // Exceptional quality
}
if caps.UptimePercentage > 0.99 {
finalScore += 0.03 // Exceptional uptime
}
if caps.ContextGeneration && caps.ContextCuration &&
caps.SemanticAnalysis && caps.ClusterCoordination {
finalScore += 0.02 // Full capability coverage
}
// Penalties
if caps.GenerationQuality < 0.5 {
finalScore -= 0.1 // Low quality
}
if caps.FailedOperations > caps.SuccessfulOperations {
finalScore -= 0.15 // High failure rate
}
```
---
## SLURP Integration
SLURP (Semantic Layer for Understanding, Reasoning, and Planning) extends election with context generation leadership.
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Base Election (Production) │
│ - Admin election │
│ - Heartbeat monitoring │
│ - Basic leadership │
└─────────────────────────────────────────────────────────────┘
│ Embeds
┌─────────────────────────────────────────────────────────────┐
│ SLURP Election (Experimental) │
│ - Context leadership │
│ - Advanced scoring │
│ - Failover state management │
│ - Health monitoring │
└─────────────────────────────────────────────────────────────┘
```
### Status: Experimental
The SLURP integration is **experimental** and provides:
- ✅ Extended candidate scoring for AI capabilities
- ✅ Context leadership state management
- ✅ Graceful failover with state preservation
- ✅ Health and metrics monitoring framework
- ⚠️ Incomplete: Actual context manager integration (TODOs present)
- ⚠️ Incomplete: State recovery mechanisms
- ⚠️ Incomplete: Production metrics collection
### Context Leadership
#### Becoming Context Leader
When a node wins election and becomes admin, it can also become context leader:
```go
func (sem *SLURPElectionManager) StartContextGeneration(ctx context.Context) error {
if !sem.IsCurrentAdmin() {
return fmt.Errorf("not admin, cannot start context generation")
}
sem.contextMu.Lock()
defer sem.contextMu.Unlock()
if sem.contextManager == nil {
return fmt.Errorf("no context manager registered")
}
// Mark as context leader
sem.isContextLeader = true
sem.contextTerm++
now := time.Now()
sem.contextStartedAt = &now
// Start background processes
sem.contextWg.Add(2)
go sem.runHealthMonitoring()
go sem.runMetricsCollection()
// Trigger callbacks
if sem.contextCallbacks != nil {
if sem.contextCallbacks.OnBecomeContextLeader != nil {
sem.contextCallbacks.OnBecomeContextLeader(ctx, sem.contextTerm)
}
if sem.contextCallbacks.OnContextGenerationStarted != nil {
sem.contextCallbacks.OnContextGenerationStarted(sem.nodeID)
}
}
// Broadcast context leadership start
// ...
return nil
}
```
#### Losing Context Leadership
When a node loses admin role or election, it stops context generation:
```go
func (sem *SLURPElectionManager) StopContextGeneration(ctx context.Context) error {
// Signal shutdown to background processes
close(sem.contextShutdown)
// Wait for background processes with timeout
done := make(chan struct{})
go func() {
sem.contextWg.Wait()
close(done)
}()
select {
case <-done:
// Clean shutdown
case <-time.After(sem.slurpConfig.GenerationStopTimeout):
// Timeout
}
sem.contextMu.Lock()
sem.isContextLeader = false
sem.contextStartedAt = nil
sem.contextMu.Unlock()
// Trigger callbacks
// ...
return nil
}
```
### Graceful Leadership Transfer
SLURP supports explicit leadership transfer with state preservation:
```go
func (sem *SLURPElectionManager) TransferContextLeadership(
ctx context.Context,
targetNodeID string,
) error {
if !sem.IsContextLeader() {
return fmt.Errorf("not context leader, cannot transfer")
}
// Prepare failover state
state, err := sem.PrepareContextFailover(ctx)
if err != nil {
return err
}
// Send transfer message to cluster
transferMsg := ElectionMessage{
Type: "context_leadership_transfer",
NodeID: sem.nodeID,
Timestamp: time.Now(),
Term: int(sem.contextTerm),
Data: map[string]interface{}{
"target_node": targetNodeID,
"failover_state": state,
"reason": "manual_transfer",
},
}
if err := sem.publishElectionMessage(transferMsg); err != nil {
return err
}
// Stop context generation
sem.StopContextGeneration(ctx)
// Trigger new election
sem.TriggerElection(TriggerManual)
return nil
}
```
### Failover State
Context leadership state preserved during failover:
```go
type ContextFailoverState struct {
// Basic failover state
LeaderID string
Term int64
TransferTime time.Time
// Context generation state
QueuedRequests []*ContextGenerationRequest
ActiveJobs map[string]*ContextGenerationJob
CompletedJobs []*ContextGenerationJob
// Cluster coordination state
ClusterState *ClusterState
ResourceAllocations map[string]*ResourceAllocation
NodeAssignments map[string][]string
// Configuration state
ManagerConfig *ManagerConfig
GenerationPolicy *GenerationPolicy
QueuePolicy *QueuePolicy
// State validation
StateVersion int64
Checksum string
HealthSnapshot *ContextClusterHealth
// Transfer metadata
TransferReason string
TransferSource string
TransferDuration time.Duration
ValidationResults *ContextStateValidation
}
```
#### State Validation
Before accepting transferred state:
```go
func (sem *SLURPElectionManager) ValidateContextState(
state *ContextFailoverState,
) (*ContextStateValidation, error) {
validation := &ContextStateValidation{
ValidatedAt: time.Now(),
ValidatedBy: sem.nodeID,
Valid: true,
}
// Check basic fields
if state.LeaderID == "" {
validation.Issues = append(validation.Issues, "missing leader ID")
validation.Valid = false
}
// Validate checksum
if state.Checksum != "" {
tempState := *state
tempState.Checksum = ""
data, _ := json.Marshal(tempState)
hash := md5.Sum(data)
expectedChecksum := fmt.Sprintf("%x", hash)
validation.ChecksumValid = (expectedChecksum == state.Checksum)
if !validation.ChecksumValid {
validation.Issues = append(validation.Issues, "checksum validation failed")
validation.Valid = false
}
}
// Validate timestamps, queue state, cluster state, config
// ...
// Set recovery requirements if issues found
if len(validation.Issues) > 0 {
validation.RequiresRecovery = true
validation.RecoverySteps = []string{
"Review validation issues",
"Perform partial state recovery",
"Restart context generation with defaults",
}
}
return validation, nil
}
```
### Health Monitoring
SLURP election includes cluster health monitoring:
```go
type ContextClusterHealth struct {
TotalNodes int
HealthyNodes int
UnhealthyNodes []string
CurrentLeader string
LeaderHealthy bool
GenerationActive bool
QueueHealth *QueueHealthStatus
NodeHealths map[string]*NodeHealthStatus
LastElection time.Time
NextHealthCheck time.Time
OverallHealthScore float64 // 0-1
}
```
Health checks run periodically (default: 30s):
```go
func (sem *SLURPElectionManager) runHealthMonitoring() {
defer sem.contextWg.Done()
ticker := time.NewTicker(sem.slurpConfig.ContextHealthCheckInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
sem.performHealthCheck()
case <-sem.contextShutdown:
return
}
}
}
```
### Configuration
```go
func DefaultSLURPElectionConfig() *SLURPElectionConfig {
return &SLURPElectionConfig{
EnableContextLeadership: true,
ContextLeadershipWeight: 0.3,
RequireContextCapability: true,
AutoStartGeneration: true,
GenerationStartDelay: 5 * time.Second,
GenerationStopTimeout: 30 * time.Second,
ContextFailoverTimeout: 60 * time.Second,
StateTransferTimeout: 30 * time.Second,
ValidationTimeout: 10 * time.Second,
RequireStateValidation: true,
ContextHealthCheckInterval: 30 * time.Second,
ClusterHealthThreshold: 0.7,
LeaderHealthThreshold: 0.8,
MaxQueueTransferSize: 1000,
QueueDrainTimeout: 60 * time.Second,
PreserveCompletedJobs: true,
CoordinationTimeout: 10 * time.Second,
MaxCoordinationRetries: 3,
CoordinationBackoff: 2 * time.Second,
}
}
```
---
## Quorum and Consensus
Currently, the election system uses **democratic voting** without strict quorum requirements. This section describes the voting mechanism and future quorum considerations.
### Voting Mechanism
#### Vote Casting
Nodes cast votes for candidates during the election period:
```go
voteMsg := ElectionMessage{
Type: "election_vote",
NodeID: voterNodeID,
Timestamp: time.Now(),
Term: currentTerm,
Data: map[string]interface{}{
"candidate": chosenCandidateID,
},
}
```
#### Vote Tallying
Votes are tallied when election timeout occurs:
```go
func (em *ElectionManager) findElectionWinner() *AdminCandidate {
if len(em.candidates) == 0 {
return nil
}
// Count votes for each candidate
voteCounts := make(map[string]int)
totalVotes := 0
for _, candidateID := range em.votes {
if _, exists := em.candidates[candidateID]; exists {
voteCounts[candidateID]++
totalVotes++
}
}
// If no votes cast, fall back to highest scoring candidate
if totalVotes == 0 {
var winner *AdminCandidate
highestScore := -1.0
for _, candidate := range em.candidates {
if candidate.Score > highestScore {
highestScore = candidate.Score
winner = candidate
}
}
return winner
}
// Find candidate with most votes (ties broken by score)
var winner *AdminCandidate
maxVotes := -1
highestScore := -1.0
for candidateID, voteCount := range voteCounts {
candidate := em.candidates[candidateID]
if voteCount > maxVotes ||
(voteCount == maxVotes && candidate.Score > highestScore) {
maxVotes = voteCount
highestScore = candidate.Score
winner = candidate
}
}
return winner
}
```
**Key Points:**
- Majority not required (simple plurality)
- If no votes cast, highest score wins (useful for single-node startup)
- Ties broken by candidate score
- Vote validation ensures voted candidate exists
### Quorum Considerations
The system does **not** currently implement strict quorum requirements. This has implications:
**Advantages:**
- Works in small clusters (1-2 nodes)
- Allows elections during network partitions
- Simple consensus algorithm
**Disadvantages:**
- Risk of split-brain if network partitions occur
- No guarantee majority of cluster agrees on admin
- Potential for competing admins in partition scenarios
**Future Enhancement:** Consider implementing configurable quorum (e.g., "majority of last known cluster size") for production deployments.
### Split-Brain Scenarios
**Scenario:** Network partition creates two isolated groups, each electing separate admin.
**Detection Methods:**
1. Admin heartbeat conflicts (multiple nodes claiming admin)
2. Cluster membership disagreements
3. Partition healing revealing duplicate admins
**Resolution:**
1. Detect conflicting admin via heartbeat messages
2. Trigger `TriggerSplitBrain` election
3. Re-elect with full cluster participation
4. Higher-scored or higher-term admin typically wins
**Mitigation:**
- Stability windows reduce rapid re-elections
- Heartbeat timeout ensures dead admin detection
- Democratic voting resolves conflicts when partition heals
---
## API Reference
### ElectionManager (Production)
#### Constructor
```go
func NewElectionManager(
ctx context.Context,
cfg *config.Config,
host libp2p.Host,
ps *pubsub.PubSub,
nodeID string,
) *ElectionManager
```
Creates new election manager.
**Parameters:**
- `ctx`: Parent context for lifecycle management
- `cfg`: CHORUS configuration (capabilities, election config)
- `host`: libp2p host for peer communication
- `ps`: PubSub instance for election messages
- `nodeID`: Unique identifier for this node
**Returns:** Configured `ElectionManager`
#### Methods
```go
func (em *ElectionManager) Start() error
```
Starts the election management system. Subscribes to election and heartbeat topics, launches discovery and coordination goroutines.
**Returns:** Error if subscription fails
---
```go
func (em *ElectionManager) Stop()
```
Stops the election manager. Stops heartbeat, cancels context, cleans up timers.
---
```go
func (em *ElectionManager) TriggerElection(trigger ElectionTrigger)
```
Manually triggers an election.
**Parameters:**
- `trigger`: Reason for triggering election (see [Election Triggers](#election-triggers))
**Behavior:**
- Respects stability windows
- Ignores if election already in progress
- Buffers trigger in channel (size 10)
---
```go
func (em *ElectionManager) GetCurrentAdmin() string
```
Returns the current admin node ID.
**Returns:** Node ID string (empty if no admin)
---
```go
func (em *ElectionManager) IsCurrentAdmin() bool
```
Checks if this node is the current admin.
**Returns:** `true` if this node is admin
---
```go
func (em *ElectionManager) GetElectionState() ElectionState
```
Returns current election state.
**Returns:** One of: `StateIdle`, `StateDiscovering`, `StateElecting`, `StateReconstructing`, `StateComplete`
---
```go
func (em *ElectionManager) SetCallbacks(
onAdminChanged func(oldAdmin, newAdmin string),
onElectionComplete func(winner string),
)
```
Sets election event callbacks.
**Parameters:**
- `onAdminChanged`: Called when admin changes (includes admin discovery)
- `onElectionComplete`: Called when election completes
---
```go
func (em *ElectionManager) SendAdminHeartbeat() error
```
Sends admin heartbeat (only if this node is admin).
**Returns:** Error if not admin or send fails
---
```go
func (em *ElectionManager) GetHeartbeatStatus() map[string]interface{}
```
Returns current heartbeat status.
**Returns:** Map with keys:
- `running` (bool): Whether heartbeat is active
- `is_admin` (bool): Whether this node is admin
- `last_sent` (time.Time): Last heartbeat time
- `interval` (string): Heartbeat interval (if running)
- `next_heartbeat` (time.Time): Next scheduled heartbeat (if running)
### SLURPElectionManager (Experimental)
#### Constructor
```go
func NewSLURPElectionManager(
ctx context.Context,
cfg *config.Config,
host libp2p.Host,
ps *pubsub.PubSub,
nodeID string,
slurpConfig *SLURPElectionConfig,
) *SLURPElectionManager
```
Creates new SLURP-enhanced election manager.
**Parameters:**
- (Same as `NewElectionManager`)
- `slurpConfig`: SLURP-specific configuration (nil for defaults)
**Returns:** Configured `SLURPElectionManager`
#### Methods
**All ElectionManager methods plus:**
```go
func (sem *SLURPElectionManager) RegisterContextManager(manager ContextManager) error
```
Registers a context manager for leader duties.
**Parameters:**
- `manager`: Context manager implementing `ContextManager` interface
**Returns:** Error if manager already registered
**Behavior:** If this node is already admin and auto-start enabled, starts context generation
---
```go
func (sem *SLURPElectionManager) IsContextLeader() bool
```
Checks if this node is the current context generation leader.
**Returns:** `true` if context leader and admin
---
```go
func (sem *SLURPElectionManager) GetContextManager() (ContextManager, error)
```
Returns the registered context manager (only if leader).
**Returns:**
- `ContextManager` if leader
- Error if not leader or no manager registered
---
```go
func (sem *SLURPElectionManager) StartContextGeneration(ctx context.Context) error
```
Begins context generation operations (leader only).
**Returns:** Error if not admin, already started, or no manager registered
**Behavior:**
- Marks node as context leader
- Increments context term
- Starts health monitoring and metrics collection
- Triggers callbacks
- Broadcasts context generation start
---
```go
func (sem *SLURPElectionManager) StopContextGeneration(ctx context.Context) error
```
Stops context generation operations.
**Returns:** Error if issues during shutdown (logged, not fatal)
**Behavior:**
- Signals background processes to stop
- Waits for clean shutdown (with timeout)
- Triggers callbacks
- Broadcasts context generation stop
---
```go
func (sem *SLURPElectionManager) TransferContextLeadership(
ctx context.Context,
targetNodeID string,
) error
```
Initiates graceful context leadership transfer.
**Parameters:**
- `ctx`: Context for transfer operations
- `targetNodeID`: Target node to receive leadership
**Returns:** Error if not leader, transfer in progress, or preparation fails
**Behavior:**
- Prepares failover state
- Broadcasts transfer message
- Stops context generation
- Triggers new election
---
```go
func (sem *SLURPElectionManager) GetContextLeaderInfo() (*LeaderInfo, error)
```
Returns information about current context leader.
**Returns:**
- `LeaderInfo` with leader details
- Error if no current leader
---
```go
func (sem *SLURPElectionManager) GetContextGenerationStatus() (*GenerationStatus, error)
```
Returns status of context operations.
**Returns:**
- `GenerationStatus` with current state
- Error if retrieval fails
---
```go
func (sem *SLURPElectionManager) SetContextLeadershipCallbacks(
callbacks *ContextLeadershipCallbacks,
) error
```
Sets callbacks for context leadership changes.
**Parameters:**
- `callbacks`: Struct with context leadership event callbacks
**Returns:** Always `nil` (error reserved for future validation)
---
```go
func (sem *SLURPElectionManager) GetContextClusterHealth() (*ContextClusterHealth, error)
```
Returns health of context generation cluster.
**Returns:** `ContextClusterHealth` with cluster health metrics
---
```go
func (sem *SLURPElectionManager) PrepareContextFailover(
ctx context.Context,
) (*ContextFailoverState, error)
```
Prepares context state for leadership failover.
**Returns:**
- `ContextFailoverState` with preserved state
- Error if not context leader or preparation fails
**Behavior:**
- Collects queued requests, active jobs, configuration
- Captures health snapshot
- Calculates checksum for validation
---
```go
func (sem *SLURPElectionManager) ExecuteContextFailover(
ctx context.Context,
state *ContextFailoverState,
) error
```
Executes context leadership failover from provided state.
**Parameters:**
- `ctx`: Context for failover operations
- `state`: Failover state from previous leader
**Returns:** Error if already leader, validation fails, or restoration fails
**Behavior:**
- Validates failover state
- Restores context leadership
- Applies configuration and state
- Starts background processes
---
```go
func (sem *SLURPElectionManager) ValidateContextState(
state *ContextFailoverState,
) (*ContextStateValidation, error)
```
Validates context failover state before accepting.
**Parameters:**
- `state`: Failover state to validate
**Returns:**
- `ContextStateValidation` with validation results
- Error only if validation process itself fails (rare)
**Validation Checks:**
- Basic field presence (LeaderID, Term, StateVersion)
- Checksum validation (MD5)
- Timestamp validity
- Queue state validity
- Cluster state validity
- Configuration validity
---
## Configuration
### Election Configuration Structure
```go
type ElectionConfig struct {
DiscoveryTimeout time.Duration // Admin discovery loop interval
DiscoveryBackoff time.Duration // Backoff after failed discovery
ElectionTimeout time.Duration // Election voting period duration
HeartbeatTimeout time.Duration // Max time without heartbeat before election
}
```
### Configuration Sources
#### 1. Config File (config.toml)
```toml
[security.election_config]
discovery_timeout = "10s"
discovery_backoff = "5s"
election_timeout = "30s"
heartbeat_timeout = "15s"
```
#### 2. Environment Variables
```bash
# Stability windows
export CHORUS_ELECTION_MIN_TERM="30s" # Min time between elections
export CHORUS_LEADER_MIN_TERM="45s" # Min time before challenging healthy leader
```
#### 3. Default Values (Fallback)
```go
// In config package
ElectionConfig: ElectionConfig{
DiscoveryTimeout: 10 * time.Second,
DiscoveryBackoff: 5 * time.Second,
ElectionTimeout: 30 * time.Second,
HeartbeatTimeout: 15 * time.Second,
}
```
### SLURP Configuration
```go
type SLURPElectionConfig struct {
// Context leadership configuration
EnableContextLeadership bool // Enable context leadership
ContextLeadershipWeight float64 // Weight for context leadership scoring
RequireContextCapability bool // Require context capability for leadership
// Context generation configuration
AutoStartGeneration bool // Auto-start generation on leadership
GenerationStartDelay time.Duration // Delay before starting generation
GenerationStopTimeout time.Duration // Timeout for stopping generation
// Failover configuration
ContextFailoverTimeout time.Duration // Context failover timeout
StateTransferTimeout time.Duration // State transfer timeout
ValidationTimeout time.Duration // State validation timeout
RequireStateValidation bool // Require state validation
// Health monitoring configuration
ContextHealthCheckInterval time.Duration // Context health check interval
ClusterHealthThreshold float64 // Minimum cluster health for operations
LeaderHealthThreshold float64 // Minimum leader health
// Queue management configuration
MaxQueueTransferSize int // Max requests to transfer
QueueDrainTimeout time.Duration // Timeout for draining queue
PreserveCompletedJobs bool // Preserve completed jobs on transfer
// Coordination configuration
CoordinationTimeout time.Duration // Coordination operation timeout
MaxCoordinationRetries int // Max coordination retries
CoordinationBackoff time.Duration // Backoff between coordination retries
}
```
**Defaults:** See `DefaultSLURPElectionConfig()` in [SLURP Integration](#slurp-integration)
---
## Message Formats
### PubSub Topics
```
CHORUS/election/v1 # Election messages (candidates, votes, winners)
CHORUS/admin/heartbeat/v1 # Admin heartbeat messages
```
### ElectionMessage Structure
```go
type ElectionMessage struct {
Type string `json:"type"` // Message type
NodeID string `json:"node_id"` // Sender node ID
Timestamp time.Time `json:"timestamp"` // Message timestamp
Term int `json:"term"` // Election term
Data interface{} `json:"data,omitempty"` // Type-specific data
}
```
### Message Types
#### 1. Admin Discovery Request
**Type:** `admin_discovery_request`
**Purpose:** Node searching for existing admin
**Data:** `nil`
**Example:**
```json
{
"type": "admin_discovery_request",
"node_id": "QmXxx...abc",
"timestamp": "2025-09-30T18:15:30.123Z",
"term": 0
}
```
#### 2. Admin Discovery Response
**Type:** `admin_discovery_response`
**Purpose:** Node informing requester of known admin
**Data:**
```json
{
"current_admin": "QmYyy...def"
}
```
**Example:**
```json
{
"type": "admin_discovery_response",
"node_id": "QmYyy...def",
"timestamp": "2025-09-30T18:15:30.456Z",
"term": 0,
"data": {
"current_admin": "QmYyy...def"
}
}
```
#### 3. Election Started
**Type:** `election_started`
**Purpose:** Node announcing start of new election
**Data:**
```json
{
"trigger": "admin_heartbeat_timeout"
}
```
**Example:**
```json
{
"type": "election_started",
"node_id": "QmXxx...abc",
"timestamp": "2025-09-30T18:15:45.123Z",
"term": 5,
"data": {
"trigger": "admin_heartbeat_timeout"
}
}
```
#### 4. Candidacy Announcement
**Type:** `candidacy_announcement`
**Purpose:** Node announcing candidacy in election
**Data:** `AdminCandidate` structure
**Example:**
```json
{
"type": "candidacy_announcement",
"node_id": "QmXxx...abc",
"timestamp": "2025-09-30T18:15:46.123Z",
"term": 5,
"data": {
"node_id": "QmXxx...abc",
"peer_id": "QmXxx...abc",
"capabilities": ["admin_election", "context_curation"],
"uptime": "86400000000000",
"resources": {
"cpu_usage": 0.35,
"memory_usage": 0.52,
"disk_usage": 0.41,
"network_quality": 0.95
},
"experience": "604800000000000",
"score": 0.78
}
}
```
#### 5. Election Vote
**Type:** `election_vote`
**Purpose:** Node casting vote for candidate
**Data:**
```json
{
"candidate": "QmYyy...def"
}
```
**Example:**
```json
{
"type": "election_vote",
"node_id": "QmZzz...ghi",
"timestamp": "2025-09-30T18:15:50.123Z",
"term": 5,
"data": {
"candidate": "QmYyy...def"
}
}
```
#### 6. Election Winner
**Type:** `election_winner`
**Purpose:** Announcing election winner
**Data:** `AdminCandidate` structure (winner)
**Example:**
```json
{
"type": "election_winner",
"node_id": "QmXxx...abc",
"timestamp": "2025-09-30T18:16:15.123Z",
"term": 5,
"data": {
"node_id": "QmYyy...def",
"peer_id": "QmYyy...def",
"capabilities": ["admin_election", "context_curation", "project_manager"],
"uptime": "172800000000000",
"resources": {
"cpu_usage": 0.25,
"memory_usage": 0.45,
"disk_usage": 0.38,
"network_quality": 0.98
},
"experience": "1209600000000000",
"score": 0.85
}
}
```
#### 7. Context Leadership Transfer (SLURP)
**Type:** `context_leadership_transfer`
**Purpose:** Graceful transfer of context leadership
**Data:**
```json
{
"target_node": "QmNewLeader...xyz",
"failover_state": { /* ContextFailoverState */ },
"reason": "manual_transfer"
}
```
#### 8. Context Generation Started (SLURP)
**Type:** `context_generation_started`
**Purpose:** Node announcing start of context generation
**Data:**
```json
{
"leader_id": "QmLeader...abc"
}
```
#### 9. Context Generation Stopped (SLURP)
**Type:** `context_generation_stopped`
**Purpose:** Node announcing stop of context generation
**Data:**
```json
{
"reason": "leadership_lost"
}
```
### Admin Heartbeat Message
**Topic:** `CHORUS/admin/heartbeat/v1`
**Format:**
```json
{
"node_id": "QmAdmin...abc",
"timestamp": "2025-09-30T18:15:30.123456789Z"
}
```
**Frequency:** Every `HeartbeatTimeout / 2` (default: ~7.5s)
**Purpose:** Prove admin liveness, prevent unnecessary elections
---
## State Machine
### Election States
```go
type ElectionState string
const (
StateIdle ElectionState = "idle"
StateDiscovering ElectionState = "discovering"
StateElecting ElectionState = "electing"
StateReconstructing ElectionState = "reconstructing_keys"
StateComplete ElectionState = "complete"
)
```
### State Transitions
```
┌─────────────────────────────────┐
│ │
│ START │
│ │
└────────────┬────────────────────┘
┌─────────────────────────────────┐
│ │
│ StateIdle │
│ - Monitoring heartbeats │
│ - Running discovery loop │
│ - Waiting for triggers │
│ │
└───┬─────────────────────────┬───┘
│ │
Discovery │ │ Election
Request │ │ Trigger
│ │
▼ ▼
┌────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ StateDiscovering │ │ StateElecting │
│ - Broadcasting discovery │ │ - Collecting candidates │
│ - Waiting for responses │ │ - Collecting votes │
│ │ │ - Election timeout running │
└────────────┬───────────────┘ └──────────┬──────────────────┘
│ │
Admin │ │ Timeout
Found │ │ Reached
│ │
▼ ▼
┌────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ Update currentAdmin │ │ StateComplete │
│ Trigger OnAdminChanged │ │ - Tallying votes │
│ Return to StateIdle │ │ - Determining winner │
│ │ │ - Broadcasting winner │
└────────────────────────────┘ └──────────┬──────────────────┘
Winner │
Announced │
┌─────────────────────────────┐
│ │
│ Update currentAdmin │
│ Start/Stop heartbeat │
│ Trigger callbacks │
│ Return to StateIdle │
│ │
└─────────────────────────────┘
```
### State Descriptions
#### StateIdle
**Description:** Normal operation state. Node is monitoring for admin heartbeats and ready to participate in elections.
**Activities:**
- Running discovery loop (periodic admin checks)
- Monitoring heartbeat timeout
- Listening for election messages
- Ready to trigger election
**Transitions:**
-`StateDiscovering`: Discovery request sent
-`StateElecting`: Election triggered
#### StateDiscovering
**Description:** Node is actively searching for existing admin.
**Activities:**
- Broadcasting discovery requests
- Waiting for discovery responses
- Timeout-based fallback to election
**Transitions:**
-`StateIdle`: Admin discovered
-`StateElecting`: No admin discovered (after timeout)
**Note:** Current implementation doesn't explicitly use this state; discovery is integrated into idle loop.
#### StateElecting
**Description:** Election in progress. Node is collecting candidates and votes.
**Activities:**
- Announcing candidacy (if eligible)
- Listening for candidate announcements
- Casting votes
- Collecting votes
- Waiting for election timeout
**Transitions:**
-`StateComplete`: Election timeout reached
**Duration:** `ElectionTimeout` (default: 30s)
#### StateComplete
**Description:** Election complete, determining winner.
**Activities:**
- Tallying votes
- Determining winner (most votes or highest score)
- Broadcasting winner
- Updating currentAdmin
- Managing heartbeat lifecycle
- Triggering callbacks
**Transitions:**
-`StateIdle`: Winner announced, system returns to normal
**Duration:** Momentary (immediate transition to `StateIdle`)
#### StateReconstructing
**Description:** Reserved for future key reconstruction operations.
**Status:** Not currently used in production code.
**Purpose:** Placeholder for post-election key reconstruction when Shamir Secret Sharing is integrated.
---
## Callbacks and Events
### Callback Types
#### 1. OnAdminChanged
**Signature:**
```go
func(oldAdmin, newAdmin string)
```
**When Called:**
- Admin discovered via discovery response
- Admin elected via election completion
- Admin changed due to re-election
**Purpose:** Notify application of admin leadership changes
**Example:**
```go
em.SetCallbacks(
func(oldAdmin, newAdmin string) {
if oldAdmin == "" {
log.Printf("✅ Admin discovered: %s", newAdmin)
} else {
log.Printf("🔄 Admin changed: %s → %s", oldAdmin, newAdmin)
}
// Update application state
app.SetCoordinator(newAdmin)
},
nil,
)
```
#### 2. OnElectionComplete
**Signature:**
```go
func(winner string)
```
**When Called:**
- Election completes and winner is determined
**Purpose:** Notify application of election completion
**Example:**
```go
em.SetCallbacks(
nil,
func(winner string) {
log.Printf("🏆 Election complete, winner: %s", winner)
// Record election in metrics
metrics.RecordElection(winner)
},
)
```
### SLURP Context Leadership Callbacks
```go
type ContextLeadershipCallbacks struct {
// Called when this node becomes context leader
OnBecomeContextLeader func(ctx context.Context, term int64) error
// Called when this node loses context leadership
OnLoseContextLeadership func(ctx context.Context, newLeader string) error
// Called when any leadership change occurs
OnContextLeaderChanged func(oldLeader, newLeader string, term int64)
// Called when context generation starts
OnContextGenerationStarted func(leaderID string)
// Called when context generation stops
OnContextGenerationStopped func(leaderID string, reason string)
// Called when context leadership failover occurs
OnContextFailover func(oldLeader, newLeader string, duration time.Duration)
// Called when context-related errors occur
OnContextError func(err error, severity ErrorSeverity)
}
```
**Example:**
```go
sem.SetContextLeadershipCallbacks(&election.ContextLeadershipCallbacks{
OnBecomeContextLeader: func(ctx context.Context, term int64) error {
log.Printf("🚀 Became context leader (term %d)", term)
return app.InitializeContextGeneration()
},
OnLoseContextLeadership: func(ctx context.Context, newLeader string) error {
log.Printf("🔄 Lost context leadership to %s", newLeader)
return app.ShutdownContextGeneration()
},
OnContextError: func(err error, severity election.ErrorSeverity) {
log.Printf("⚠️ Context error [%s]: %v", severity, err)
if severity == election.ErrorSeverityCritical {
app.TriggerFailover()
}
},
})
```
### Callback Threading
**Important:** Callbacks are invoked from election manager goroutines. Consider:
1. **Non-Blocking:** Callbacks should be fast or spawn goroutines for slow operations
2. **Error Handling:** Errors in callbacks are logged but don't prevent election operations
3. **Synchronization:** Use proper locking if callbacks modify shared state
4. **Idempotency:** Callbacks may be invoked multiple times for same event (rare but possible)
**Good Practice:**
```go
em.SetCallbacks(
func(oldAdmin, newAdmin string) {
// Fast: Update local state
app.mu.Lock()
app.currentAdmin = newAdmin
app.mu.Unlock()
// Slow: Spawn goroutine for heavy work
go app.NotifyAdminChange(oldAdmin, newAdmin)
},
nil,
)
```
---
## Testing
### Test Structure
The package includes comprehensive unit tests in `election_test.go`.
### Running Tests
```bash
# Run all election tests
cd /home/tony/chorus/project-queues/active/CHORUS
go test ./pkg/election
# Run with verbose output
go test -v ./pkg/election
# Run specific test
go test -v ./pkg/election -run TestElectionManagerCanBeAdmin
# Run with race detection
go test -race ./pkg/election
```
### Test Utilities
#### newTestElectionManager
```go
func newTestElectionManager(t *testing.T) *ElectionManager
```
Creates a fully-wired test election manager with:
- Real libp2p host (localhost)
- Real PubSub instance
- Test configuration
- Automatic cleanup
**Example:**
```go
func TestMyFeature(t *testing.T) {
em := newTestElectionManager(t)
// Test uses real message passing
em.Start()
// ... test code ...
// Cleanup automatic via t.Cleanup()
}
```
### Test Coverage
#### 1. TestNewElectionManagerInitialState
Verifies initial state after construction:
- State is `StateIdle`
- Term is `0`
- Node ID is populated
#### 2. TestElectionManagerCanBeAdmin
Tests eligibility checking:
- Node with admin capabilities can be admin
- Node without admin capabilities cannot be admin
#### 3. TestFindElectionWinnerPrefersVotesThenScore
Tests winner determination logic:
- Most votes wins
- Score breaks ties
- Fallback to highest score if no votes
#### 4. TestHandleElectionMessageAddsCandidate
Tests candidacy announcement handling:
- Candidate added to candidates map
- Candidate data correctly deserialized
#### 5. TestSendAdminHeartbeatRequiresLeadership
Tests heartbeat authorization:
- Non-admin cannot send heartbeat
- Admin can send heartbeat
### Integration Testing
For integration testing with multiple nodes:
```go
func TestMultiNodeElection(t *testing.T) {
// Create 3 test nodes
nodes := make([]*ElectionManager, 3)
for i := 0; i < 3; i++ {
nodes[i] = newTestElectionManager(t)
nodes[i].Start()
}
// Connect nodes (libp2p peer connection)
// ...
// Trigger election
nodes[0].TriggerElection(TriggerManual)
// Wait for election to complete
time.Sleep(35 * time.Second)
// Verify all nodes agree on admin
admin := nodes[0].GetCurrentAdmin()
for i, node := range nodes {
if node.GetCurrentAdmin() != admin {
t.Errorf("Node %d disagrees on admin", i)
}
}
}
```
**Note:** Multi-node tests require proper libp2p peer discovery and connection setup.
---
## Production Considerations
### Deployment Checklist
#### Configuration
- [ ] Set appropriate `HeartbeatTimeout` (default 15s)
- [ ] Set appropriate `ElectionTimeout` (default 30s)
- [ ] Configure stability windows via environment variables
- [ ] Ensure nodes have correct capabilities in config
- [ ] Configure discovery and backoff timeouts
#### Monitoring
- [ ] Monitor election frequency (should be rare)
- [ ] Monitor heartbeat status on admin node
- [ ] Alert on frequent admin changes (possible network issues)
- [ ] Track election duration and participation
- [ ] Monitor candidate scores and voting patterns
#### Network
- [ ] Ensure PubSub connectivity between all nodes
- [ ] Configure appropriate gossipsub parameters
- [ ] Test behavior during network partitions
- [ ] Verify heartbeat messages reach all nodes
- [ ] Monitor libp2p connection stability
#### Capabilities
- [ ] Ensure at least one node has admin capabilities
- [ ] Balance capabilities across cluster (redundancy)
- [ ] Test elections with different capability distributions
- [ ] Verify scoring weights match organizational priorities
#### Resource Metrics
- [ ] Implement actual resource metric collection (currently simulated)
- [ ] Calibrate resource scoring weights
- [ ] Test behavior under high load
- [ ] Verify low-resource nodes don't become admin
### Performance Characteristics
#### Latency
- **Discovery Response:** < 1s (network RTT + processing)
- **Election Duration:** `ElectionTimeout` + processing (~30-35s)
- **Heartbeat Latency:** < 1s (network RTT)
- **Admin Failover:** `HeartbeatTimeout` + `ElectionTimeout` (~45s)
#### Scalability
- **Tested:** 1-10 nodes
- **Expected:** 10-100 nodes (limited by gossipsub performance)
- **Bottleneck:** PubSub message fanout, JSON serialization overhead
#### Message Load
Per election cycle:
- Discovery: 1 request + N responses
- Election: 1 start + N candidacies + N votes + 1 winner = ~3N+2 messages
- Heartbeat: 1 message every ~7.5s from admin
### Common Issues and Solutions
#### Issue: Rapid Election Churn
**Symptoms:** Elections occurring frequently, admin changing constantly
**Causes:**
- Network instability
- Insufficient stability windows
- Admin node resource exhaustion
**Solutions:**
1. Increase stability windows:
```bash
export CHORUS_ELECTION_MIN_TERM="60s"
export CHORUS_LEADER_MIN_TERM="90s"
```
2. Investigate network connectivity
3. Check admin node resources
4. Review scoring weights (prefer stable nodes)
#### Issue: Split-Brain (Multiple Admins)
**Symptoms:** Different nodes report different admins
**Causes:**
- Network partition
- PubSub message loss
- No quorum enforcement
**Solutions:**
1. Trigger manual election to force re-sync:
```go
em.TriggerElection(TriggerManual)
```
2. Verify network connectivity
3. Consider implementing quorum (future enhancement)
#### Issue: No Admin Elected
**Symptoms:** All nodes report empty admin
**Causes:**
- No nodes have admin capabilities
- Election timeout too short
- PubSub not properly connected
**Solutions:**
1. Verify at least one node has capabilities:
```toml
capabilities = ["admin_election", "context_curation"]
```
2. Increase `ElectionTimeout`
3. Check PubSub subscription status
4. Verify nodes are connected in libp2p mesh
#### Issue: Admin Heartbeat Not Received
**Symptoms:** Frequent heartbeat timeout elections despite admin running
**Causes:**
- PubSub message loss
- Heartbeat goroutine stopped
- Clock skew
**Solutions:**
1. Check heartbeat status:
```go
status := em.GetHeartbeatStatus()
log.Printf("Heartbeat status: %+v", status)
```
2. Verify PubSub connectivity
3. Check admin node logs for heartbeat errors
4. Ensure NTP synchronization across cluster
### Security Considerations
#### Authentication
**Current State:** Election messages are not authenticated beyond libp2p peer IDs.
**Risk:** Malicious node could announce false election results.
**Mitigation:**
- Rely on libp2p transport security
- Future: Sign election messages with node private keys
- Future: Verify candidate claims against cluster membership
#### Authorization
**Current State:** Any node with admin capabilities can participate in elections.
**Risk:** Compromised node could win election and become admin.
**Mitigation:**
- Carefully control which nodes have admin capabilities
- Monitor election outcomes for suspicious patterns
- Future: Implement capability attestation
- Future: Add reputation scoring
#### Split-Brain Attacks
**Current State:** No strict quorum, partitions can elect separate admins.
**Risk:** Adversary could isolate admin and force minority election.
**Mitigation:**
- Use stability windows to prevent rapid changes
- Monitor for conflicting admin announcements
- Future: Implement configurable quorum requirements
- Future: Add partition detection and recovery
#### Message Spoofing
**Current State:** PubSub messages are authenticated by libp2p but content is not signed.
**Risk:** Man-in-the-middle could modify election messages.
**Mitigation:**
- Use libp2p transport security (TLS)
- Future: Add message signing with node keys
- Future: Implement message sequence numbers
### SLURP Production Readiness
**Status:** Experimental - Not recommended for production
**Incomplete Features:**
- Context manager integration (TODOs present)
- State recovery mechanisms
- Production metrics collection
- Comprehensive failover testing
**Production Use:** Wait for:
1. Context manager interface stabilization
2. Complete state recovery implementation
3. Production metrics and monitoring
4. Multi-node failover testing
5. Documentation of recovery procedures
---
## Summary
The CHORUS election package provides democratic leader election for distributed agent clusters. Key highlights:
### Production Features (Ready)
✅ **Democratic Voting:** Uptime and capability-based candidate scoring
✅ **Heartbeat Monitoring:** 5s interval, 15s timeout for liveness detection
✅ **Automatic Failover:** Elections triggered on timeout, split-brain, manual
✅ **Stability Windows:** Prevents election churn during network instability
✅ **Clean Transitions:** Callback system for graceful leadership handoffs
✅ **Well-Tested:** Comprehensive unit tests with real libp2p integration
### Experimental Features (Not Production-Ready)
⚠️ **SLURP Integration:** Context leadership with advanced AI scoring
⚠️ **Failover State:** Graceful transfer with state preservation
⚠️ **Health Monitoring:** Cluster health tracking framework
### Key Metrics
- **Discovery Cycle:** 10s (configurable)
- **Heartbeat Interval:** ~7.5s (HeartbeatTimeout / 2)
- **Heartbeat Timeout:** 15s (triggers election)
- **Election Duration:** 30s (voting period)
- **Failover Time:** ~45s (timeout + election)
### Recommended Configuration
```toml
[security.election_config]
discovery_timeout = "10s"
election_timeout = "30s"
heartbeat_timeout = "15s"
```
```bash
export CHORUS_ELECTION_MIN_TERM="30s"
export CHORUS_LEADER_MIN_TERM="45s"
```
### Next Steps for Production
1. **Implement Resource Metrics:** Replace simulated metrics with actual system monitoring
2. **Add Quorum Support:** Implement configurable quorum for split-brain prevention
3. **Complete SLURP Integration:** Finish context manager integration and state recovery
4. **Enhanced Security:** Add message signing and capability attestation
5. **Comprehensive Testing:** Multi-node integration tests with partition scenarios
---
**Documentation Version:** 1.0
**Last Updated:** 2025-09-30
**Package Version:** Based on commit at documentation time
**Maintainer:** CHORUS Development Team