← back to Dw Boardroom Governance
src/agents/escalation.ts
130 lines
/**
* Escalation Agent — 12-hour threshold monitor
* Ported from /root/DW-Agents/boardroom-agent/lib/escalation.js
*
* Checks every 5 minutes for pending decisions past threshold.
* Logs escalations to gov_escalation_log.
*/
import { query, execute } from '../db/db';
import { broadcast } from '../ws/broadcast';
import { Decision } from '../types';
const CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
const DEFAULT_THRESHOLD_HOURS = 12;
let escalationTimer: NodeJS.Timeout | null = null;
export async function getThresholdHours(): Promise<number> {
const row = await query<{ value: any }>(
"SELECT value FROM gov_config WHERE key = 'escalation_threshold_hours'"
);
return row[0] ? Number(row[0].value) : DEFAULT_THRESHOLD_HOURS;
}
export async function checkEscalations(): Promise<{
escalated: number;
checked: number;
}> {
const thresholdHours = await getThresholdHours();
// Find pending decisions older than threshold
const overdue = await query<Decision>(
`SELECT * FROM gov_decision_log
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '1 hour' * $1
AND id NOT IN (
SELECT decision_id FROM gov_escalation_log
WHERE decision_id IS NOT NULL
AND created_at > NOW() - INTERVAL '2 hours'
)
ORDER BY created_at ASC
LIMIT 10`,
[thresholdHours]
);
let escalated = 0;
for (const decision of overdue) {
const hoursElapsed = (Date.now() - new Date(decision.created_at).getTime()) / (1000 * 60 * 60);
await query(
`INSERT INTO gov_escalation_log (decision_id, escalation_type, hours_elapsed, action_taken, escalated_to)
VALUES ($1, $2, $3, $4, $5)`,
[
decision.id,
`${thresholdHours}hr`,
Math.round(hoursElapsed * 10) / 10,
'auto_escalated',
'human_operator',
]
);
// Upgrade decision type to C (Controlled = requires human)
if (decision.decision_type !== 'C') {
await execute(
"UPDATE gov_decision_log SET decision_type = 'C', reasoning = COALESCE(reasoning, '') || ' [Auto-escalated after ' || $1 || 'h]' WHERE id = $2",
[Math.round(hoursElapsed), decision.id]
);
}
broadcast('escalation', {
decisionId: decision.id,
topic: decision.topic,
hoursElapsed: Math.round(hoursElapsed * 10) / 10,
});
escalated++;
console.log(`[Escalation] Decision #${decision.id} escalated after ${Math.round(hoursElapsed)}h`);
}
return { escalated, checked: overdue.length };
}
export async function getActiveEscalations(): Promise<any[]> {
return query(
`SELECT e.*, d.topic, d.owner, d.status as decision_status, d.decision_type
FROM gov_escalation_log e
LEFT JOIN gov_decision_log d ON e.decision_id = d.id
WHERE d.status = 'pending'
ORDER BY e.created_at DESC
LIMIT 20`
);
}
export async function getEscalationLog(limit: number = 50): Promise<any[]> {
return query(
`SELECT e.*, d.topic, d.owner
FROM gov_escalation_log e
LEFT JOIN gov_decision_log d ON e.decision_id = d.id
ORDER BY e.created_at DESC
LIMIT $1`,
[limit]
);
}
export function startEscalationMonitor(): void {
if (escalationTimer) return;
escalationTimer = setInterval(async () => {
try {
const result = await checkEscalations();
if (result.escalated > 0) {
console.log(`[Escalation] Cycle: ${result.escalated} escalated of ${result.checked} checked`);
}
} catch (err: any) {
console.error('[Escalation] Check error:', err.message);
}
}, CHECK_INTERVAL_MS);
console.log(`[Escalation] Monitor started (check every ${CHECK_INTERVAL_MS / 60000}min)`);
}
export function stopEscalationMonitor(): void {
if (escalationTimer) {
clearInterval(escalationTimer);
escalationTimer = null;
console.log('[Escalation] Monitor stopped');
}
}