/** * Incidents Service * Based on: docs/foundation/Agent_Governance_Protocol_v1.md * * Handles incident creation, escalation, and resolution */ import { db } from '../../infra/db/client'; import { logger } from '../../infra/logger/logger'; import { v4 as uuidv4 } from 'uuid'; import { Incident, IncidentStatus, IncidentPriority, EscalationLevel, TargetScopeType, IncidentHistory, CreateIncidentRequest, AssignIncidentRequest, EscalateIncidentRequest, ResolveIncidentRequest, } from '../../domain/governance/types'; import { governanceService } from './governance.service'; interface IncidentRow { id: string; created_by_dais_id: string; target_scope_type: TargetScopeType; target_scope_id: string; status: IncidentStatus; priority: IncidentPriority; assigned_to_dais_id: string | null; escalation_level: EscalationLevel; title: string; description: string | null; resolution: string | null; metadata: Record; created_at: Date; updated_at: Date; resolved_at: Date | null; closed_at: Date | null; } export class IncidentsService { /** * Create a new incident */ async createIncident(request: CreateIncidentRequest): Promise { const id = uuidv4(); // Determine initial escalation level based on target scope const escalationLevel = this.determineInitialEscalation(request.targetScopeType); try { const result = await db.query( `INSERT INTO incidents ( id, created_by_dais_id, target_scope_type, target_scope_id, status, priority, escalation_level, title, description, metadata ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`, [ id, request.createdByDaisId, request.targetScopeType, request.targetScopeId, 'open', request.priority || 'medium', escalationLevel, request.title, request.description || null, JSON.stringify(request.metadata || {}), ] ); const incident = this.mapRowToIncident(result.rows[0]); // Log history await this.addHistory(id, 'created', request.createdByDaisId, null, { title: request.title, priority: request.priority || 'medium', escalationLevel, }); // Log governance event await governanceService.logEvent( 'incident.created', request.createdByDaisId, id, `${request.targetScopeType}:${request.targetScopeId}`, { title: request.title, priority: request.priority } ); logger.info(`Incident created: ${id}`, { title: request.title }); return incident; } catch (error) { logger.error('Failed to create incident', error); throw error; } } /** * Get incident by ID */ async getIncident(incidentId: string): Promise { const result = await db.query( `SELECT * FROM incidents WHERE id = $1`, [incidentId] ); if (result.rows.length === 0) { return null; } return this.mapRowToIncident(result.rows[0]); } /** * List incidents with filters */ async listIncidents(filters: { status?: IncidentStatus; priority?: IncidentPriority; escalationLevel?: EscalationLevel; targetScopeType?: TargetScopeType; targetScopeId?: string; assignedToDaisId?: string; limit?: number; offset?: number; }): Promise<{ incidents: Incident[]; total: number }> { const conditions: string[] = []; const params: (string | number)[] = []; let paramIndex = 1; if (filters.status) { conditions.push(`status = $${paramIndex++}`); params.push(filters.status); } if (filters.priority) { conditions.push(`priority = $${paramIndex++}`); params.push(filters.priority); } if (filters.escalationLevel) { conditions.push(`escalation_level = $${paramIndex++}`); params.push(filters.escalationLevel); } if (filters.targetScopeType) { conditions.push(`target_scope_type = $${paramIndex++}`); params.push(filters.targetScopeType); } if (filters.targetScopeId) { conditions.push(`target_scope_id = $${paramIndex++}`); params.push(filters.targetScopeId); } if (filters.assignedToDaisId) { conditions.push(`assigned_to_dais_id = $${paramIndex++}`); params.push(filters.assignedToDaisId); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Get total count const countResult = await db.query<{ count: string }>( `SELECT COUNT(*) as count FROM incidents ${whereClause}`, params ); const total = parseInt(countResult.rows[0].count, 10); // Get incidents const limit = filters.limit || 50; const offset = filters.offset || 0; params.push(limit, offset); const result = await db.query( `SELECT * FROM incidents ${whereClause} ORDER BY CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 END, created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`, params ); const incidents = result.rows.map(row => this.mapRowToIncident(row)); return { incidents, total }; } /** * Assign incident to an agent */ async assignIncident(request: AssignIncidentRequest): Promise<{ success: boolean; error?: string }> { try { const incident = await this.getIncident(request.incidentId); if (!incident) { return { success: false, error: 'Incident not found' }; } if (incident.status === 'closed') { return { success: false, error: 'Cannot assign closed incident' }; } const oldValue = { assignedToDaisId: incident.assignedToDaisId }; await db.query( `UPDATE incidents SET assigned_to_dais_id = $1, status = 'in_progress', updated_at = now() WHERE id = $2`, [request.assignedToDaisId, request.incidentId] ); await this.addHistory( request.incidentId, 'assigned', request.actorDaisId, oldValue, { assignedToDaisId: request.assignedToDaisId } ); await governanceService.logEvent( 'incident.assigned', request.actorDaisId, request.incidentId, 'city', { assignedTo: request.assignedToDaisId } ); logger.info(`Incident assigned: ${request.incidentId} → ${request.assignedToDaisId}`); return { success: true }; } catch (error) { logger.error('Failed to assign incident', error); throw error; } } /** * Escalate incident to higher level */ async escalateIncident(request: EscalateIncidentRequest): Promise<{ success: boolean; error?: string }> { try { const incident = await this.getIncident(request.incidentId); if (!incident) { return { success: false, error: 'Incident not found' }; } if (incident.status === 'closed' || incident.status === 'resolved') { return { success: false, error: 'Cannot escalate resolved/closed incident' }; } const levelOrder: EscalationLevel[] = ['microdao', 'district', 'city']; const currentIndex = levelOrder.indexOf(incident.escalationLevel); const newIndex = levelOrder.indexOf(request.newLevel); if (newIndex <= currentIndex) { return { success: false, error: 'Can only escalate to higher level' }; } const oldValue = { escalationLevel: incident.escalationLevel }; await db.query( `UPDATE incidents SET escalation_level = $1, updated_at = now() WHERE id = $2`, [request.newLevel, request.incidentId] ); await this.addHistory( request.incidentId, 'escalated', request.actorDaisId, oldValue, { escalationLevel: request.newLevel, reason: request.reason } ); await governanceService.logEvent( 'incident.escalated', request.actorDaisId, request.incidentId, 'city', { fromLevel: incident.escalationLevel, toLevel: request.newLevel, reason: request.reason, } ); logger.info(`Incident escalated: ${request.incidentId} → ${request.newLevel}`); return { success: true }; } catch (error) { logger.error('Failed to escalate incident', error); throw error; } } /** * Resolve incident */ async resolveIncident(request: ResolveIncidentRequest): Promise<{ success: boolean; error?: string }> { try { const incident = await this.getIncident(request.incidentId); if (!incident) { return { success: false, error: 'Incident not found' }; } if (incident.status === 'closed' || incident.status === 'resolved') { return { success: false, error: 'Incident already resolved/closed' }; } await db.query( `UPDATE incidents SET status = 'resolved', resolution = $1, resolved_at = now(), updated_at = now() WHERE id = $2`, [request.resolution, request.incidentId] ); await this.addHistory( request.incidentId, 'resolved', request.actorDaisId, { status: incident.status }, { status: 'resolved', resolution: request.resolution } ); await governanceService.logEvent( 'incident.resolved', request.actorDaisId, request.incidentId, 'city', { resolution: request.resolution } ); logger.info(`Incident resolved: ${request.incidentId}`); return { success: true }; } catch (error) { logger.error('Failed to resolve incident', error); throw error; } } /** * Close incident */ async closeIncident( incidentId: string, actorDaisId: string ): Promise<{ success: boolean; error?: string }> { try { const incident = await this.getIncident(incidentId); if (!incident) { return { success: false, error: 'Incident not found' }; } await db.query( `UPDATE incidents SET status = 'closed', closed_at = now(), updated_at = now() WHERE id = $1`, [incidentId] ); await this.addHistory( incidentId, 'closed', actorDaisId, { status: incident.status }, { status: 'closed' } ); await governanceService.logEvent( 'incident.closed', actorDaisId, incidentId, 'city', {} ); logger.info(`Incident closed: ${incidentId}`); return { success: true }; } catch (error) { logger.error('Failed to close incident', error); throw error; } } /** * Add comment to incident */ async addComment( incidentId: string, actorDaisId: string, comment: string ): Promise<{ success: boolean; error?: string }> { try { await this.addHistory(incidentId, 'comment', actorDaisId, null, null, comment); logger.info(`Comment added to incident: ${incidentId}`); return { success: true }; } catch (error) { logger.error('Failed to add comment to incident', error); throw error; } } /** * Get incident history */ async getIncidentHistory(incidentId: string): Promise { const result = await db.query<{ id: string; incident_id: string; action: IncidentHistory['action']; actor_dais_id: string; old_value: Record | null; new_value: Record | null; comment: string | null; created_at: Date; }>( `SELECT * FROM incident_history WHERE incident_id = $1 ORDER BY created_at ASC`, [incidentId] ); return result.rows.map(row => ({ id: row.id, incidentId: row.incident_id, action: row.action, actorDaisId: row.actor_dais_id, oldValue: row.old_value || undefined, newValue: row.new_value || undefined, comment: row.comment || undefined, createdAt: row.created_at, })); } /** * Get open incidents count by escalation level */ async getOpenIncidentsCount(): Promise<{ microdao: number; district: number; city: number; total: number; }> { const result = await db.query<{ escalation_level: EscalationLevel; count: string }>( `SELECT escalation_level, COUNT(*) as count FROM incidents WHERE status IN ('open', 'in_progress') GROUP BY escalation_level` ); const counts = { microdao: 0, district: 0, city: 0, total: 0 }; result.rows.forEach(row => { counts[row.escalation_level] = parseInt(row.count, 10); counts.total += parseInt(row.count, 10); }); return counts; } /** * Determine initial escalation level based on target scope */ private determineInitialEscalation(targetScopeType: TargetScopeType): EscalationLevel { switch (targetScopeType) { case 'city': return 'city'; case 'district': return 'district'; default: return 'microdao'; } } /** * Add entry to incident history */ private async addHistory( incidentId: string, action: IncidentHistory['action'], actorDaisId: string, oldValue: Record | null, newValue: Record | null, comment?: string ): Promise { await db.query( `INSERT INTO incident_history (incident_id, action, actor_dais_id, old_value, new_value, comment) VALUES ($1, $2, $3, $4, $5, $6)`, [ incidentId, action, actorDaisId, oldValue ? JSON.stringify(oldValue) : null, newValue ? JSON.stringify(newValue) : null, comment || null, ] ); } /** * Map database row to Incident type */ private mapRowToIncident(row: IncidentRow): Incident { return { id: row.id, createdByDaisId: row.created_by_dais_id, targetScopeType: row.target_scope_type, targetScopeId: row.target_scope_id, status: row.status, priority: row.priority, assignedToDaisId: row.assigned_to_dais_id || undefined, escalationLevel: row.escalation_level, title: row.title, description: row.description || undefined, resolution: row.resolution || undefined, metadata: row.metadata, createdAt: row.created_at, updatedAt: row.updated_at, resolvedAt: row.resolved_at || undefined, closedAt: row.closed_at || undefined, }; } } export const incidentsService = new IncidentsService();