import { NextRequest, NextResponse } from 'next/server'; import { getDatabase, Notification, db_helpers } from '@/lib/db'; import { runOpenClaw } from '@/lib/command'; import { requireRole } from '@/lib/auth'; import { logger } from '@/lib/logger'; /** * POST /api/notifications/deliver - Notification delivery daemon endpoint * * Polls undelivered notifications and sends them to agent sessions * via OpenClaw sessions_send command */ export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); try { const db = getDatabase(); const body = await request.json(); const workspaceId = auth.user.workspace_id ?? 1; const { agent_filter, // Optional: only deliver to specific agent limit = 50, // Max notifications to process per call dry_run = false // Test mode - don't actually deliver } = body; // Get undelivered notifications let query = ` SELECT n.*, a.session_key FROM notifications n LEFT JOIN agents a ON n.recipient = a.name AND a.workspace_id = n.workspace_id WHERE n.delivered_at IS NULL AND n.workspace_id = ? `; const params: any[] = [workspaceId]; if (agent_filter) { query += ' AND n.recipient = ?'; params.push(agent_filter); } query += ' ORDER BY n.created_at ASC LIMIT ?'; params.push(limit); const undeliveredNotifications = db.prepare(query).all(...params) as (Notification & { session_key?: string })[]; if (undeliveredNotifications.length === 0) { return NextResponse.json({ status: 'success', message: 'No undelivered notifications found', processed: 0, delivered: 0, errors: [] }); } let deliveredCount = 0; let errorCount = 0; const errors: any[] = []; const deliveryResults: any[] = []; // Prepare update statement once (avoids N+1) const markDeliveredStmt = db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ? AND workspace_id = ?'); for (const notification of undeliveredNotifications) { try { // Skip if agent doesn't have session key if (!notification.session_key) { errors.push({ notification_id: notification.id, recipient: notification.recipient, error: 'Agent has no session key configured' }); errorCount++; continue; } // Format message for delivery const message = formatNotificationMessage(notification); if (!dry_run) { // Send notification via OpenClaw sessions_send try { const { stdout, stderr } = await runOpenClaw( [ 'gateway', 'sessions_send', '--session', notification.session_key, '--message', message ], { timeoutMs: 10000 } ); if (stderr && stderr.includes('error')) { throw new Error(`OpenClaw error: ${stderr}`); } // Mark as delivered const now = Math.floor(Date.now() / 1000); markDeliveredStmt.run(now, notification.id, workspaceId); deliveredCount++; deliveryResults.push({ notification_id: notification.id, recipient: notification.recipient, session_key: notification.session_key, delivered_at: now, status: 'delivered', stdout: stdout.substring(0, 200) // Truncate for storage }); // Log successful delivery db_helpers.logActivity( 'notification_delivered', 'notification', notification.id, 'system', `Notification delivered to ${notification.recipient}`, { notification_type: notification.type, session_key: notification.session_key, title: notification.title }, workspaceId ); } catch (cmdError: any) { throw new Error(`Command failed: ${cmdError.message}`); } } else { // Dry run - just log what would be sent deliveryResults.push({ notification_id: notification.id, recipient: notification.recipient, session_key: notification.session_key, status: 'dry_run', message: message }); deliveredCount++; } } catch (error: any) { errorCount++; errors.push({ notification_id: notification.id, recipient: notification.recipient, error: error.message }); logger.error({ err: error, notificationId: notification.id, recipient: notification.recipient }, 'Failed to deliver notification'); } } // Log delivery batch summary db_helpers.logActivity( 'notification_delivery_batch', 'system', 0, 'notification_daemon', `Processed ${undeliveredNotifications.length} notifications: ${deliveredCount} delivered, ${errorCount} failed`, { total_processed: undeliveredNotifications.length, delivered: deliveredCount, errors: errorCount, dry_run, agent_filter: agent_filter || null }, workspaceId ); return NextResponse.json({ status: 'success', message: `Processed ${undeliveredNotifications.length} notifications`, total_processed: undeliveredNotifications.length, delivered: deliveredCount, errors: errorCount, dry_run, delivery_results: deliveryResults, error_details: errors }); } catch (error) { logger.error({ err: error }, 'POST /api/notifications/deliver error'); return NextResponse.json({ error: 'Failed to deliver notifications' }, { status: 500 }); } } /** * GET /api/notifications/deliver - Get delivery status and statistics */ export async function GET(request: NextRequest) { const auth = requireRole(request, 'viewer') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) try { const db = getDatabase(); const { searchParams } = new URL(request.url); const workspaceId = auth.user.workspace_id ?? 1; const agent = searchParams.get('agent'); // Get delivery statistics let baseQuery = 'SELECT COUNT(*) as count FROM notifications WHERE workspace_id = ?'; let params: any[] = [workspaceId]; if (agent) { baseQuery += ' AND recipient = ?'; params.push(agent); } const totalNotifications = db.prepare(baseQuery).get(...params) as { count: number }; const undeliveredCount = db.prepare( baseQuery + ' AND delivered_at IS NULL' ).get(...params) as { count: number }; const deliveredCount = db.prepare( baseQuery + ' AND delivered_at IS NOT NULL' ).get(...params) as { count: number }; // Get recent delivery activity const recentDeliveries = db.prepare(` SELECT recipient, type, title, delivered_at, created_at FROM notifications WHERE delivered_at IS NOT NULL AND workspace_id = ? ${agent ? 'AND recipient = ?' : ''} ORDER BY delivered_at DESC LIMIT 10 `).all(...(agent ? [workspaceId, agent] : [workspaceId])); // Get agents with pending notifications const agentsPending = db.prepare(` SELECT n.recipient, a.session_key, COUNT(*) as pending_count FROM notifications n LEFT JOIN agents a ON n.recipient = a.name AND a.workspace_id = n.workspace_id WHERE n.delivered_at IS NULL AND n.workspace_id = ? GROUP BY n.recipient, a.session_key ORDER BY pending_count DESC `).all(workspaceId) as any[]; return NextResponse.json({ statistics: { total: totalNotifications.count, delivered: deliveredCount.count, undelivered: undeliveredCount.count, delivery_rate: totalNotifications.count > 0 ? Math.round((deliveredCount.count / totalNotifications.count) * 100) : 0 }, agents_with_pending: agentsPending, recent_deliveries: recentDeliveries, agent_filter: agent }); } catch (error) { logger.error({ err: error }, 'GET /api/notifications/deliver error'); return NextResponse.json({ error: 'Failed to get delivery status' }, { status: 500 }); } } /** * Format notification for delivery to agent session */ function formatNotificationMessage(notification: Notification): string { const timestamp = new Date(notification.created_at * 1000).toLocaleString(); let message = `🔔 **${notification.title}**\n\n`; message += `${notification.message}\n\n`; if (notification.type === 'mention') { message += `📝 You were mentioned in a comment\n`; } else if (notification.type === 'assignment') { message += `📋 You have been assigned a new task\n`; } else if (notification.type === 'due_date') { message += `⏰ Task deadline approaching\n`; } if (notification.source_type && notification.source_id) { message += `🔗 Related ${notification.source_type} ID: ${notification.source_id}\n`; } message += `⏰ ${timestamp}`; return message; }