package orchestrator import ( "context" "fmt" "sync" "time" "github.com/chorus-services/whoosh/internal/agents" "github.com/chorus-services/whoosh/internal/composer" "github.com/chorus-services/whoosh/internal/council" "github.com/docker/docker/api/types/swarm" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" "github.com/rs/zerolog/log" ) // AgentDeployer manages deployment of agent containers for teams type AgentDeployer struct { swarmManager *SwarmManager db *pgxpool.Pool registry string ctx context.Context cancel context.CancelFunc constraintMu sync.Mutex } // NewAgentDeployer creates a new agent deployer func NewAgentDeployer(swarmManager *SwarmManager, db *pgxpool.Pool, registry string) *AgentDeployer { ctx, cancel := context.WithCancel(context.Background()) if registry == "" { registry = "registry.home.deepblack.cloud" } return &AgentDeployer{ swarmManager: swarmManager, db: db, registry: registry, ctx: ctx, cancel: cancel, } } // Close shuts down the agent deployer func (ad *AgentDeployer) Close() error { ad.cancel() return nil } // DeploymentRequest represents a request to deploy agents for a team type DeploymentRequest struct { TeamID uuid.UUID `json:"team_id"` TaskID uuid.UUID `json:"task_id"` TeamComposition *composer.TeamComposition `json:"team_composition"` TaskContext *TaskContext `json:"task_context"` DeploymentMode string `json:"deployment_mode"` // immediate, scheduled, manual } // DeploymentResult represents the result of a deployment operation type DeploymentResult struct { TeamID uuid.UUID `json:"team_id"` TaskID uuid.UUID `json:"task_id"` DeployedServices []DeployedService `json:"deployed_services"` Status string `json:"status"` // success, partial, failed Message string `json:"message"` DeployedAt time.Time `json:"deployed_at"` Errors []string `json:"errors,omitempty"` } // DeployedService represents a successfully deployed service type DeployedService struct { ServiceID string `json:"service_id"` ServiceName string `json:"service_name"` AgentRole string `json:"agent_role"` AgentID string `json:"agent_id"` Image string `json:"image"` Status string `json:"status"` } // CouncilDeploymentRequest represents a request to deploy council agents type CouncilDeploymentRequest struct { CouncilID uuid.UUID `json:"council_id"` ProjectName string `json:"project_name"` CouncilComposition *council.CouncilComposition `json:"council_composition"` ProjectContext *CouncilProjectContext `json:"project_context"` DeploymentMode string `json:"deployment_mode"` // immediate, scheduled, manual } // CouncilProjectContext contains the project information for council agents type CouncilProjectContext struct { ProjectName string `json:"project_name"` Repository string `json:"repository"` ProjectBrief string `json:"project_brief"` Constraints string `json:"constraints,omitempty"` TechLimits string `json:"tech_limits,omitempty"` ComplianceNotes string `json:"compliance_notes,omitempty"` Targets string `json:"targets,omitempty"` ExternalURL string `json:"external_url,omitempty"` } // DeployTeamAgents deploys all agents for a team func (ad *AgentDeployer) DeployTeamAgents(request *DeploymentRequest) (*DeploymentResult, error) { log.Info(). Str("team_id", request.TeamID.String()). Str("task_id", request.TaskID.String()). Int("agent_matches", len(request.TeamComposition.AgentMatches)). Msg("🚀 Starting team agent deployment") result := &DeploymentResult{ TeamID: request.TeamID, TaskID: request.TaskID, DeployedServices: []DeployedService{}, DeployedAt: time.Now(), Errors: []string{}, } // Deploy each agent in the team composition for _, agentMatch := range request.TeamComposition.AgentMatches { service, err := ad.deploySingleAgent(request, agentMatch) if err != nil { errorMsg := fmt.Sprintf("Failed to deploy agent %s for role %s: %v", agentMatch.Agent.Name, agentMatch.Role.Name, err) result.Errors = append(result.Errors, errorMsg) log.Error(). Err(err). Str("agent_id", agentMatch.Agent.ID.String()). Str("role", agentMatch.Role.Name). Msg("Failed to deploy agent") continue } deployedService := DeployedService{ ServiceID: service.ID, ServiceName: service.Spec.Name, AgentRole: agentMatch.Role.Name, AgentID: agentMatch.Agent.ID.String(), Image: service.Spec.TaskTemplate.ContainerSpec.Image, Status: "deploying", } result.DeployedServices = append(result.DeployedServices, deployedService) // Update database with deployment info err = ad.recordDeployment(request.TeamID, request.TaskID, agentMatch, service.ID) if err != nil { log.Error(). Err(err). Str("service_id", service.ID). Msg("Failed to record deployment in database") } } // Determine overall deployment status if len(result.Errors) == 0 { result.Status = "success" result.Message = fmt.Sprintf("Successfully deployed %d agents", len(result.DeployedServices)) } else if len(result.DeployedServices) > 0 { result.Status = "partial" result.Message = fmt.Sprintf("Deployed %d/%d agents with %d errors", len(result.DeployedServices), len(request.TeamComposition.AgentMatches), len(result.Errors)) } else { result.Status = "failed" result.Message = "Failed to deploy any agents" } // Update team deployment status in database err := ad.updateTeamDeploymentStatus(request.TeamID, result.Status, result.Message) if err != nil { log.Error(). Err(err). Str("team_id", request.TeamID.String()). Msg("Failed to update team deployment status") } log.Info(). Str("team_id", request.TeamID.String()). Str("status", result.Status). Int("deployed", len(result.DeployedServices)). Int("errors", len(result.Errors)). Msg("✅ Team agent deployment completed") return result, nil } // selectAgentImage determines the appropriate CHORUS image for the agent role func (ad *AgentDeployer) selectAgentImage(roleName string, agent *composer.Agent) string { // All agents use the same CHORUS image, but with different configurations // The image handles role specialization internally based on environment variables return "docker.io/anthonyrawlins/chorus:backbeat-v2.0.1" } // buildAgentEnvironment creates environment variables for CHORUS agent configuration func (ad *AgentDeployer) buildAgentEnvironment(request *DeploymentRequest, agentMatch *composer.AgentMatch) map[string]string { env := map[string]string{ // Core CHORUS configuration - just pass the agent name from human-roles.yaml // CHORUS will handle its own prompt composition and system behavior "CHORUS_AGENT_NAME": agentMatch.Role.Name, // This maps to human-roles.yaml agent definition "CHORUS_TEAM_ID": request.TeamID.String(), "CHORUS_TASK_ID": request.TaskID.String(), // Essential task context "CHORUS_PROJECT": request.TaskContext.Repository, "CHORUS_TASK_TITLE": request.TaskContext.IssueTitle, "CHORUS_TASK_DESC": request.TaskContext.IssueDescription, "CHORUS_PRIORITY": request.TaskContext.Priority, "CHORUS_EXTERNAL_URL": request.TaskContext.ExternalURL, // WHOOSH coordination "WHOOSH_COORDINATOR": "true", "WHOOSH_ENDPOINT": "http://whoosh:8080", // Docker access for CHORUS sandbox management "DOCKER_HOST": "unix:///var/run/docker.sock", } return env } // Note: CHORUS handles its own prompt composition from human-roles.yaml // We just need to pass the agent name and essential task context // determineAgentType maps role to agent type for resource allocation func (ad *AgentDeployer) determineAgentType(agentMatch *composer.AgentMatch) string { // Simple mapping for now - could be enhanced based on role complexity return "standard" } // calculateResources determines resource requirements for the agent func (ad *AgentDeployer) calculateResources(agentMatch *composer.AgentMatch) ResourceLimits { // Standard resource allocation for CHORUS agents // CHORUS handles its own resource management internally return ResourceLimits{ CPULimit: 1000000000, // 1 CPU core MemoryLimit: 1073741824, // 1GB RAM CPURequest: 500000000, // 0.5 CPU core MemoryRequest: 536870912, // 512MB RAM } } // buildAgentVolumes creates volume mounts for CHORUS agents func (ad *AgentDeployer) buildAgentVolumes(request *DeploymentRequest) []VolumeMount { return []VolumeMount{ { Type: "bind", Source: "/var/run/docker.sock", Target: "/var/run/docker.sock", ReadOnly: false, // CHORUS needs Docker access for sandboxing }, { Type: "volume", Source: fmt.Sprintf("whoosh-workspace-%s", request.TeamID.String()), Target: "/workspace", ReadOnly: false, }, } } // buildAgentPlacement creates placement constraints for agents func (ad *AgentDeployer) buildAgentPlacement(agentMatch *composer.AgentMatch) PlacementConfig { return PlacementConfig{ Constraints: []string{ "node.role==worker", // Prefer worker nodes for agent containers }, // Note: Placement preferences removed for compilation compatibility } } // deploySingleAgent deploys a single agent for a specific role func (ad *AgentDeployer) deploySingleAgent(request *DeploymentRequest, agentMatch *composer.AgentMatch) (*swarm.Service, error) { // Determine agent image based on role image := ad.selectAgentImage(agentMatch.Role.Name, agentMatch.Agent) // Build deployment configuration config := &AgentDeploymentConfig{ TeamID: request.TeamID.String(), TaskID: request.TaskID.String(), AgentRole: agentMatch.Role.Name, AgentType: ad.determineAgentType(agentMatch), Image: image, Replicas: 1, // Start with single replica per agent Resources: ad.calculateResources(agentMatch), Environment: ad.buildAgentEnvironment(request, agentMatch), TaskContext: *request.TaskContext, Networks: []string{"chorus_default"}, Volumes: ad.buildAgentVolumes(request), Placement: ad.buildAgentPlacement(agentMatch), } // Deploy the service service, err := ad.swarmManager.DeployAgent(config) if err != nil { return nil, fmt.Errorf("failed to deploy agent service: %w", err) } return service, nil } // recordDeployment records agent deployment information in the database func (ad *AgentDeployer) recordDeployment(teamID uuid.UUID, taskID uuid.UUID, agentMatch *composer.AgentMatch, serviceID string) error { query := ` INSERT INTO agent_deployments (team_id, task_id, agent_id, role_id, service_id, status, deployed_at) VALUES ($1, $2, $3, $4, $5, $6, NOW()) ` _, err := ad.db.Exec(ad.ctx, query, teamID, taskID, agentMatch.Agent.ID, agentMatch.Role.ID, serviceID, "deployed") return err } // updateTeamDeploymentStatus updates the team deployment status in the database func (ad *AgentDeployer) updateTeamDeploymentStatus(teamID uuid.UUID, status, message string) error { query := ` UPDATE teams SET deployment_status = $1, deployment_message = $2, updated_at = NOW() WHERE id = $3 ` _, err := ad.db.Exec(ad.ctx, query, status, message, teamID) return err } // AssignCouncilAgents assigns council roles to available CHORUS agents instead of deploying new services func (ad *AgentDeployer) AssignCouncilAgents(request *CouncilDeploymentRequest) (*council.CouncilDeploymentResult, error) { log.Info(). Str("council_id", request.CouncilID.String()). Str("project_name", request.ProjectName). Int("core_agents", len(request.CouncilComposition.CoreAgents)). Int("optional_agents", len(request.CouncilComposition.OptionalAgents)). Msg("🎭 Starting council agent assignment to available CHORUS agents") result := &council.CouncilDeploymentResult{ CouncilID: request.CouncilID, ProjectName: request.ProjectName, DeployedAgents: []council.DeployedCouncilAgent{}, DeployedAt: time.Now(), Errors: []string{}, } // Get available CHORUS agents from the registry availableAgents, err := ad.getAvailableChorusAgents() if err != nil { return result, fmt.Errorf("failed to get available CHORUS agents: %w", err) } if len(availableAgents) == 0 { result.Status = "failed" result.Message = "No available CHORUS agents found for council assignment" result.Errors = append(result.Errors, "No available agents broadcasting availability") return result, fmt.Errorf("no available CHORUS agents for council formation") } log.Info(). Int("available_agents", len(availableAgents)). Msg("Found available CHORUS agents for council assignment") // Assign core agents (required) assignedCount := 0 for _, councilAgent := range request.CouncilComposition.CoreAgents { if assignedCount >= len(availableAgents) { errorMsg := fmt.Sprintf("Not enough available agents for role %s - need %d more agents", councilAgent.RoleName, len(request.CouncilComposition.CoreAgents)+len(request.CouncilComposition.OptionalAgents)-assignedCount) result.Errors = append(result.Errors, errorMsg) break } // Select next available agent chorusAgent := availableAgents[assignedCount] // Assign the council role to this CHORUS agent deployedAgent, err := ad.assignRoleToChorusAgent(request, councilAgent, chorusAgent) if err != nil { errorMsg := fmt.Sprintf("Failed to assign role %s to agent %s: %v", councilAgent.RoleName, chorusAgent.Name, err) result.Errors = append(result.Errors, errorMsg) log.Error(). Err(err). Str("council_agent_id", councilAgent.AgentID). Str("chorus_agent_id", chorusAgent.ID.String()). Str("role", councilAgent.RoleName). Msg("Failed to assign council role to CHORUS agent") continue } result.DeployedAgents = append(result.DeployedAgents, *deployedAgent) assignedCount++ // Update database with assignment info err = ad.recordCouncilAgentAssignment(request.CouncilID, councilAgent, chorusAgent.ID.String()) if err != nil { log.Error(). Err(err). Str("chorus_agent_id", chorusAgent.ID.String()). Msg("Failed to record council agent assignment in database") } } // Assign optional agents (best effort) for _, councilAgent := range request.CouncilComposition.OptionalAgents { if assignedCount >= len(availableAgents) { log.Info(). Str("role", councilAgent.RoleName). Msg("No more available agents for optional council role") break } // Select next available agent chorusAgent := availableAgents[assignedCount] // Assign the optional council role to this CHORUS agent deployedAgent, err := ad.assignRoleToChorusAgent(request, councilAgent, chorusAgent) if err != nil { // Optional agents failing is not critical log.Warn(). Err(err). Str("council_agent_id", councilAgent.AgentID). Str("chorus_agent_id", chorusAgent.ID.String()). Str("role", councilAgent.RoleName). Msg("Failed to assign optional council role (non-critical)") continue } result.DeployedAgents = append(result.DeployedAgents, *deployedAgent) assignedCount++ // Update database with assignment info err = ad.recordCouncilAgentAssignment(request.CouncilID, councilAgent, chorusAgent.ID.String()) if err != nil { log.Error(). Err(err). Str("chorus_agent_id", chorusAgent.ID.String()). Msg("Failed to record council agent assignment in database") } } // Determine overall assignment status coreAgentsCount := len(request.CouncilComposition.CoreAgents) assignedCoreAgents := 0 for _, deployedAgent := range result.DeployedAgents { // Check if this assigned agent is a core agent for _, coreAgent := range request.CouncilComposition.CoreAgents { if coreAgent.RoleName == deployedAgent.RoleName { assignedCoreAgents++ break } } } if assignedCoreAgents == coreAgentsCount { result.Status = "success" result.Message = fmt.Sprintf("Successfully assigned %d agents (%d core, %d optional) to council roles", len(result.DeployedAgents), assignedCoreAgents, len(result.DeployedAgents)-assignedCoreAgents) } else if assignedCoreAgents > 0 { result.Status = "partial" result.Message = fmt.Sprintf("Assigned %d/%d core agents with %d errors", assignedCoreAgents, coreAgentsCount, len(result.Errors)) } else { result.Status = "failed" result.Message = "Failed to assign any core council agents" } // Update council assignment status in database err = ad.updateCouncilDeploymentStatus(request.CouncilID, result.Status, result.Message) if err != nil { log.Error(). Err(err). Str("council_id", request.CouncilID.String()). Msg("Failed to update council assignment status") } log.Info(). Str("council_id", request.CouncilID.String()). Str("status", result.Status). Int("assigned", len(result.DeployedAgents)). Int("errors", len(result.Errors)). Msg("✅ Council agent assignment completed") return result, nil } // deploySingleCouncilAgent deploys a single council agent func (ad *AgentDeployer) deploySingleCouncilAgent(request *CouncilDeploymentRequest, agent council.CouncilAgent) (*council.DeployedCouncilAgent, error) { // Use the CHORUS image for all council agents image := "docker.io/anthonyrawlins/chorus:backbeat-v2.0.1" // Build council-specific deployment configuration config := &AgentDeploymentConfig{ TeamID: request.CouncilID.String(), // Use council ID as team ID TaskID: request.CouncilID.String(), // Use council ID as task ID AgentRole: agent.RoleName, AgentType: "council", Image: image, Replicas: 1, // Single replica per council agent Resources: ad.calculateCouncilResources(agent), Environment: ad.buildCouncilAgentEnvironment(request, agent), TaskContext: TaskContext{ Repository: request.ProjectContext.Repository, IssueTitle: request.ProjectContext.ProjectName, IssueDescription: request.ProjectContext.ProjectBrief, Priority: "high", // Council formation is always high priority ExternalURL: request.ProjectContext.ExternalURL, }, Networks: []string{"chorus_default"}, // Connect to CHORUS network Volumes: ad.buildCouncilAgentVolumes(request), Placement: ad.buildCouncilAgentPlacement(agent), } // Deploy the service service, err := ad.swarmManager.DeployAgent(config) if err != nil { return nil, fmt.Errorf("failed to deploy council agent service: %w", err) } // Create deployed agent result deployedAgent := &council.DeployedCouncilAgent{ ServiceID: service.ID, ServiceName: service.Spec.Name, RoleName: agent.RoleName, AgentID: agent.AgentID, Image: image, Status: "deploying", DeployedAt: time.Now(), } return deployedAgent, nil } // buildCouncilAgentEnvironment creates environment variables for council agent configuration func (ad *AgentDeployer) buildCouncilAgentEnvironment(request *CouncilDeploymentRequest, agent council.CouncilAgent) map[string]string { env := map[string]string{ // Core CHORUS configuration for council mode "CHORUS_AGENT_NAME": agent.RoleName, // Maps to human-roles.yaml agent definition "CHORUS_COUNCIL_MODE": "true", // Enable council mode "CHORUS_COUNCIL_ID": request.CouncilID.String(), "CHORUS_PROJECT_NAME": request.ProjectContext.ProjectName, // Council prompt and context "CHORUS_COUNCIL_PROMPT": "/app/prompts/council.md", "CHORUS_PROJECT_BRIEF": request.ProjectContext.ProjectBrief, "CHORUS_CONSTRAINTS": request.ProjectContext.Constraints, "CHORUS_TECH_LIMITS": request.ProjectContext.TechLimits, "CHORUS_COMPLIANCE_NOTES": request.ProjectContext.ComplianceNotes, "CHORUS_TARGETS": request.ProjectContext.Targets, // Essential project context "CHORUS_PROJECT": request.ProjectContext.Repository, "CHORUS_EXTERNAL_URL": request.ProjectContext.ExternalURL, "CHORUS_PRIORITY": "high", // WHOOSH coordination "WHOOSH_COORDINATOR": "true", "WHOOSH_ENDPOINT": "http://whoosh:8080", // Docker access for CHORUS sandbox management "DOCKER_HOST": "unix:///var/run/docker.sock", } return env } // calculateCouncilResources determines resource requirements for council agents func (ad *AgentDeployer) calculateCouncilResources(agent council.CouncilAgent) ResourceLimits { // Council agents get slightly more resources since they handle complex analysis return ResourceLimits{ CPULimit: 1500000000, // 1.5 CPU cores MemoryLimit: 2147483648, // 2GB RAM CPURequest: 750000000, // 0.75 CPU core MemoryRequest: 1073741824, // 1GB RAM } } // buildCouncilAgentVolumes creates volume mounts for council agents func (ad *AgentDeployer) buildCouncilAgentVolumes(request *CouncilDeploymentRequest) []VolumeMount { return []VolumeMount{ { Type: "bind", Source: "/var/run/docker.sock", Target: "/var/run/docker.sock", ReadOnly: false, // Council agents need Docker access for complex setup }, { Type: "volume", Source: fmt.Sprintf("whoosh-council-%s", request.CouncilID.String()), Target: "/workspace", ReadOnly: false, }, { Type: "bind", Source: "/rust/containers/WHOOSH/prompts", Target: "/app/prompts", ReadOnly: true, // Mount council prompts }, } } // buildCouncilAgentPlacement creates placement constraints for council agents func (ad *AgentDeployer) buildCouncilAgentPlacement(agent council.CouncilAgent) PlacementConfig { return PlacementConfig{ Constraints: []string{ "node.role==worker", // Prefer worker nodes for council containers }, } } // recordCouncilAgentDeployment records council agent deployment information in the database func (ad *AgentDeployer) recordCouncilAgentDeployment(councilID uuid.UUID, agent council.CouncilAgent, serviceID string) error { query := ` UPDATE council_agents SET deployed = true, status = 'active', service_id = $1, deployed_at = NOW(), updated_at = NOW() WHERE council_id = $2 AND agent_id = $3 ` _, err := ad.db.Exec(ad.ctx, query, serviceID, councilID, agent.AgentID) return err } // updateCouncilDeploymentStatus updates the council deployment status in the database func (ad *AgentDeployer) updateCouncilDeploymentStatus(councilID uuid.UUID, status, message string) error { query := ` UPDATE councils SET status = $1, updated_at = NOW() WHERE id = $2 ` // Map deployment status to council status councilStatus := "active" if status == "failed" { councilStatus = "failed" } else if status == "partial" { councilStatus = "active" // Partial deployment still allows council to function } _, err := ad.db.Exec(ad.ctx, query, councilStatus, councilID) return err } // getAvailableChorusAgents gets available CHORUS agents from the registry func (ad *AgentDeployer) getAvailableChorusAgents() ([]*agents.DatabaseAgent, error) { // Create a registry instance to access available agents registry := agents.NewRegistry(ad.db, nil) // No p2p discovery needed for querying // Get available agents from the database availableAgents, err := registry.GetAvailableAgents(ad.ctx) if err != nil { return nil, fmt.Errorf("failed to query available agents: %w", err) } log.Info(). Int("available_count", len(availableAgents)). Msg("Retrieved available CHORUS agents from registry") return availableAgents, nil } // assignRoleToChorusAgent assigns a council role to an available CHORUS agent func (ad *AgentDeployer) assignRoleToChorusAgent(request *CouncilDeploymentRequest, councilAgent council.CouncilAgent, chorusAgent *agents.DatabaseAgent) (*council.DeployedCouncilAgent, error) { // For now, we'll create a "virtual" assignment without actually deploying anything // The CHORUS agents will receive role assignments via P2P messaging in a future implementation // This approach uses the existing agent infrastructure instead of creating new services log.Info(). Str("council_role", councilAgent.RoleName). Str("chorus_agent_id", chorusAgent.ID.String()). Str("chorus_agent_name", chorusAgent.Name). Msg("🎯 Assigning council role to available CHORUS agent") // Create a deployed agent record that represents the assignment deployedAgent := &council.DeployedCouncilAgent{ ServiceID: fmt.Sprintf("assigned-%s", chorusAgent.ID.String()), // Virtual service ID ServiceName: fmt.Sprintf("council-%s", councilAgent.RoleName), RoleName: councilAgent.RoleName, AgentID: chorusAgent.ID.String(), // Use the actual CHORUS agent ID Image: "chorus:assigned", // Indicate this is an assignment, not a deployment Status: "assigned", // Different from "deploying" to indicate assignment approach DeployedAt: time.Now(), } // TODO: In a future implementation, send role assignment via P2P messaging // This would involve: // 1. Publishing a role assignment message to the P2P network // 2. The target CHORUS agent receiving and acknowledging the assignment // 3. The agent reconfiguring itself with the new council role // 4. The agent updating its availability status to reflect the new role log.Info(). Str("assignment_id", deployedAgent.ServiceID). Str("role", deployedAgent.RoleName). Str("agent", deployedAgent.AgentID). Msg("✅ Council role assigned to CHORUS agent") return deployedAgent, nil } // recordCouncilAgentAssignment records council agent assignment in the database func (ad *AgentDeployer) recordCouncilAgentAssignment(councilID uuid.UUID, councilAgent council.CouncilAgent, chorusAgentID string) error { query := ` UPDATE council_agents SET deployed = true, status = 'assigned', service_id = $1, deployed_at = NOW(), updated_at = NOW() WHERE council_id = $2 AND agent_id = $3 ` // Use the chorus agent ID as the "service ID" to track the assignment assignmentID := fmt.Sprintf("assigned-%s", chorusAgentID) retry := false execUpdate := func() error { _, err := ad.db.Exec(ad.ctx, query, assignmentID, councilID, councilAgent.AgentID) return err } err := execUpdate() if err != nil { if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "23514" { retry = true log.Warn(). Str("council_id", councilID.String()). Str("role", councilAgent.RoleName). Str("agent", councilAgent.AgentID). Msg("Council agent assignment hit legacy status constraint – attempting auto-remediation") if ensureErr := ad.ensureCouncilAgentStatusConstraint(); ensureErr != nil { return fmt.Errorf("failed to reconcile council agent status constraint: %w", ensureErr) } err = execUpdate() } } if err != nil { return fmt.Errorf("failed to record council agent assignment: %w", err) } if retry { log.Info(). Str("council_id", councilID.String()). Str("role", councilAgent.RoleName). Msg("Council agent status constraint updated to support 'assigned' state") } log.Debug(). Str("council_id", councilID.String()). Str("council_agent_id", councilAgent.AgentID). Str("chorus_agent_id", chorusAgentID). Str("role", councilAgent.RoleName). Msg("Recorded council agent assignment in database") return nil } func (ad *AgentDeployer) ensureCouncilAgentStatusConstraint() error { ad.constraintMu.Lock() defer ad.constraintMu.Unlock() tx, err := ad.db.BeginTx(ad.ctx, pgx.TxOptions{}) if err != nil { return fmt.Errorf("begin council agent status constraint update: %w", err) } dropStmt := `ALTER TABLE council_agents DROP CONSTRAINT IF EXISTS council_agents_status_check` if _, err := tx.Exec(ad.ctx, dropStmt); err != nil { tx.Rollback(ad.ctx) return fmt.Errorf("drop council agent status constraint: %w", err) } addStmt := `ALTER TABLE council_agents ADD CONSTRAINT council_agents_status_check CHECK (status IN ('pending', 'deploying', 'assigned', 'active', 'failed', 'removed'))` if _, err := tx.Exec(ad.ctx, addStmt); err != nil { tx.Rollback(ad.ctx) if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "42710" { // Constraint already exists with desired definition; treat as success. return nil } return fmt.Errorf("add council agent status constraint: %w", err) } if err := tx.Commit(ad.ctx); err != nil { return fmt.Errorf("commit council agent status constraint update: %w", err) } return nil }