← back to Dw Boardroom Governance
src/engine/meetingEngine.ts
206 lines
/**
* Meeting Engine — Phase orchestrator
* Ported from /root/DW-Agents/boardroom-agent/lib/meeting-engine.js
*
* 8-phase structure: Caucus → Call to Order → Old Business → Current Business
* → Breakout → New Business → Minutes → Adjournment
*
* Features:
* - Mutex lock (one meeting at a time)
* - Phase timeouts (10min adhoc, 20min scheduled)
* - WebSocket broadcast on phase transitions
*/
import { query, queryOne, execute } from '../db/db';
import { broadcast } from '../ws/broadcast';
import { Meeting, MeetingPhase, MEETING_PHASES, MeetingStatus, AgendaItem } from '../types';
import { v4 as uuidv4 } from 'uuid';
// Mutex: only one meeting at a time
let activeMeetingId: string | null = null;
let phaseTimer: NodeJS.Timeout | null = null;
const PHASE_DURATION_MS: Record<string, number> = {
caucus: 60_000,
call_to_order: 30_000,
old_business: 120_000,
current_business: 120_000,
breakout: 90_000,
new_business: 120_000,
minutes: 60_000,
adjournment: 30_000,
};
const MAX_MEETING_DURATION_MS: Record<string, number> = {
emergency: 10 * 60_000,
adhoc: 10 * 60_000,
strategic: 20 * 60_000,
initiative: 15 * 60_000,
ops: 15 * 60_000,
standup: 10 * 60_000,
};
export function isRunning(): boolean {
return activeMeetingId !== null;
}
export function getActiveMeetingId(): string | null {
return activeMeetingId;
}
export async function startMeeting(meetingType: string, agendaItems?: AgendaItem[]): Promise<Meeting> {
if (activeMeetingId) {
throw new Error('Meeting already in progress. Halt current meeting first.');
}
const id = uuidv4();
const now = new Date().toISOString();
await query(
`INSERT INTO gov_meetings (id, meeting_type, phase, started_at, status, action_items)
VALUES ($1, $2, $3, $4, $5, $6)`,
[id, meetingType, 'caucus', now, 'in_progress', JSON.stringify(agendaItems || [])]
);
activeMeetingId = id;
// Add opening message
await addMessage(id, 'system', 'Governance', `${meetingType} meeting called to order.`, 'system');
broadcast('meeting_started', { meetingId: id, meetingType, phase: 'caucus' });
console.log(`[Meeting] Started ${meetingType} meeting: ${id}`);
// Start auto-advance timer
schedulePhaseAdvance(id, 'caucus');
return (await queryOne<Meeting>('SELECT * FROM gov_meetings WHERE id = $1', [id]))!;
}
export async function advancePhase(meetingId: string): Promise<MeetingPhase | null> {
const meeting = await queryOne<Meeting>('SELECT * FROM gov_meetings WHERE id = $1', [meetingId]);
if (!meeting || meeting.status !== 'in_progress') return null;
const currentIdx = MEETING_PHASES.indexOf(meeting.phase as MeetingPhase);
if (currentIdx === -1 || currentIdx >= MEETING_PHASES.length - 1) {
// Last phase — complete the meeting
await completeMeeting(meetingId);
return null;
}
const nextPhase = MEETING_PHASES[currentIdx + 1];
await execute(
'UPDATE gov_meetings SET phase = $1 WHERE id = $2',
[nextPhase, meetingId]
);
await addMessage(meetingId, 'system', 'Governance', `Phase: ${formatPhase(nextPhase)}`, 'phase_change');
broadcast('meeting_phase', { meetingId, phase: nextPhase, phaseIndex: currentIdx + 1 });
console.log(`[Meeting] ${meetingId} → ${nextPhase}`);
// Schedule next advance
schedulePhaseAdvance(meetingId, nextPhase);
return nextPhase;
}
export async function haltMeeting(meetingId: string): Promise<void> {
if (phaseTimer) {
clearTimeout(phaseTimer);
phaseTimer = null;
}
await execute(
"UPDATE gov_meetings SET status = 'halted', ended_at = NOW() WHERE id = $1",
[meetingId]
);
await addMessage(meetingId, 'system', 'Governance', 'Meeting halted by operator.', 'system');
broadcast('meeting_halted', { meetingId });
if (activeMeetingId === meetingId) {
activeMeetingId = null;
}
console.log(`[Meeting] Halted: ${meetingId}`);
}
async function completeMeeting(meetingId: string): Promise<void> {
if (phaseTimer) {
clearTimeout(phaseTimer);
phaseTimer = null;
}
await execute(
"UPDATE gov_meetings SET status = 'completed', phase = 'adjournment', ended_at = NOW() WHERE id = $1",
[meetingId]
);
await addMessage(meetingId, 'system', 'Governance', 'Meeting adjourned.', 'system');
broadcast('meeting_completed', { meetingId });
if (activeMeetingId === meetingId) {
activeMeetingId = null;
}
console.log(`[Meeting] Completed: ${meetingId}`);
}
export async function addMessage(
meetingId: string,
agentId: string,
agentName: string,
message: string,
messageType: string = 'update'
): Promise<void> {
await query(
`INSERT INTO gov_meeting_messages (meeting_id, agent_id, agent_name, message, message_type)
VALUES ($1, $2, $3, $4, $5)`,
[meetingId, agentId, agentName, message, messageType]
);
broadcast('meeting_message', { meetingId, agentId, agentName, message, messageType });
}
function schedulePhaseAdvance(meetingId: string, phase: string): void {
if (phaseTimer) clearTimeout(phaseTimer);
// Auto-advance after this phase's configured duration
const duration = PHASE_DURATION_MS[phase] || 120_000;
phaseTimer = setTimeout(async () => {
try {
const meeting = await queryOne<Meeting>('SELECT * FROM gov_meetings WHERE id = $1', [meetingId]);
if (meeting && meeting.status === 'in_progress') {
await advancePhase(meetingId);
}
} catch (err: any) {
console.error('[Meeting] Auto-advance error:', err.message);
}
}, duration);
}
function formatPhase(phase: string): string {
return phase.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
// ─── Queries ─────────────────────────────────────
export async function listMeetings(limit: number = 20): Promise<Meeting[]> {
return query<Meeting>(
'SELECT * FROM gov_meetings ORDER BY created_at DESC LIMIT $1',
[limit]
);
}
export async function getMeeting(id: string): Promise<Meeting | null> {
return queryOne<Meeting>('SELECT * FROM gov_meetings WHERE id = $1', [id]);
}
export async function getMessages(meetingId: string): Promise<any[]> {
return query(
'SELECT * FROM gov_meeting_messages WHERE meeting_id = $1 ORDER BY created_at ASC',
[meetingId]
);
}