/ src / lib / server / runtime / daemon-policy.ts
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  }