daemon-policy.ts
1 import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults' 2 import { loadSettings } from '@/lib/server/settings/settings-repository' 3 import type { Session } from '@/types' 4 5 const SYNTHETIC_HEALTH_SESSION_USERS = new Set(['workbench', 'comparison-bench']) 6 const SYNTHETIC_HEALTH_SESSION_PREFIXES = ['wb-', 'cmp-'] 7 8 function parseBoolish(value: unknown, fallback: boolean): boolean { 9 if (typeof value === 'boolean') return value 10 if (typeof value !== 'string') return fallback 11 const normalized = value.trim().toLowerCase() 12 if (!normalized) return fallback 13 if (['1', 'true', 'yes', 'on'].includes(normalized)) return true 14 if (['0', 'false', 'no', 'off'].includes(normalized)) return false 15 return fallback 16 } 17 18 export function daemonAutostartEnvEnabled(): boolean { 19 if (typeof process.env.SWARMCLAW_DAEMON_AUTOSTART === 'string' && process.env.SWARMCLAW_DAEMON_AUTOSTART.trim()) { 20 return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, true) 21 } 22 const settings = loadSettings() 23 return parseBoolish(settings.daemonAutostartEnabled, true) 24 } 25 26 export function isDaemonBackgroundServicesEnabled(): boolean { 27 return parseBoolish(process.env.SWARMCLAW_DAEMON_BACKGROUND_SERVICES, true) 28 } 29 30 export function parseHeartbeatIntervalSec( 31 value: unknown, 32 fallback = DEFAULT_HEARTBEAT_INTERVAL_SEC, 33 ): number { 34 const parsed = typeof value === 'number' 35 ? value 36 : typeof value === 'string' 37 ? Number.parseInt(value, 10) 38 : Number.NaN 39 if (!Number.isFinite(parsed)) return fallback 40 return Math.max(0, Math.min(3600, Math.trunc(parsed))) 41 } 42 43 export function shouldNotifyProviderReachabilityIssue(provider: string): boolean { 44 return provider !== 'openclaw' 45 } 46 47 function hasSyntheticHealthPrefix(value: unknown): boolean { 48 const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '' 49 return SYNTHETIC_HEALTH_SESSION_PREFIXES.some((prefix) => normalized.startsWith(prefix)) 50 } 51 52 export function shouldSuppressSessionHeartbeatHealthAlert( 53 session: Pick<Session, 'id' | 'name' | 'user' | 'shortcutForAgentId'>, 54 ): boolean { 55 const user = typeof session.user === 'string' ? session.user.trim().toLowerCase() : '' 56 if (SYNTHETIC_HEALTH_SESSION_USERS.has(user)) return true 57 if (hasSyntheticHealthPrefix(session.id)) return true 58 if (hasSyntheticHealthPrefix(session.shortcutForAgentId)) return true 59 60 const name = typeof session.name === 'string' ? session.name.trim().toLowerCase() : '' 61 return name.startsWith('workbench ') 62 || name.startsWith('assistant benchmark ') 63 || name.startsWith('comparison ') 64 } 65 66 export function shouldSuppressSyntheticAgentHealthAlert(agentId: string): boolean { 67 return hasSyntheticHealthPrefix(agentId) 68 } 69 70 export function buildSessionHeartbeatHealthDedupKey( 71 sessionId: string, 72 state: 'stale' | 'auto-disabled', 73 ): string { 74 return `health-alert:session-heartbeat:${state}:${sessionId}` 75 } 76 77 export function parseCronToMs(cron: string | null | undefined, fallbackMs: number): number | null { 78 if (!cron || typeof cron !== 'string') return null 79 const hourMatch = cron.match(/\*\/(\d+)/) 80 if (hourMatch) return parseInt(hourMatch[1], 10) * 3600_000 81 return fallbackMs 82 }