/** * Unified event bus — aggregates runtime events (tasks, scheduler, telegram, * webhooks, tool calls) into a single rolling log for context injection and * the ContextHUD. * * Pattern mirrors devduck's event_bus: any module fires a custom event with * a standard shape, the bus captures it into a ring buffer, and consumers * (system prompt builder, HUD, replay) read from it. */ export interface BusEvent { id: string timestamp: number source: string // 'task' | 'scheduler' | 'telegram' | 'webhook' | 'tool' | string kind: 'started' | 'completed' | 'error' | 'received' | 'sent' | 'fired' | 'info' | 'spawned' | 'killed' | 'updated' summary: string // short human-readable data?: any // optional structured payload } const MAX_EVENTS = 200 const STORAGE_KEY = 'careless-event-bus' class EventBus { private events: BusEvent[] = [] private listeners: Set<(evs: BusEvent[]) => void> = new Set() private inited = false init() { if (this.inited) return this.inited = true // Hydrate from sessionStorage try { const raw = sessionStorage.getItem(STORAGE_KEY) if (raw) this.events = JSON.parse(raw).slice(-MAX_EVENTS) } catch {} // Global listener for careless:bus events window.addEventListener('careless:bus', (e: any) => { const payload = e.detail if (payload && payload.summary) this.push(payload) }) } push(ev: Omit & Partial>) { const full: BusEvent = { id: ev.id || `be-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, timestamp: ev.timestamp || Date.now(), source: ev.source, kind: ev.kind, summary: ev.summary, data: ev.data, } this.events.push(full) if (this.events.length > MAX_EVENTS) this.events = this.events.slice(-MAX_EVENTS) try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(this.events)) } catch {} this.listeners.forEach(fn => fn(this.events)) } recent(limit = 20, maxAgeMs = 5 * 60 * 1000): BusEvent[] { const cutoff = Date.now() - maxAgeMs return this.events.filter(e => e.timestamp >= cutoff).slice(-limit) } all(): BusEvent[] { return [...this.events] } clear() { this.events = [] try { sessionStorage.removeItem(STORAGE_KEY) } catch {} this.listeners.forEach(fn => fn(this.events)) } subscribe(fn: (evs: BusEvent[]) => void): () => void { this.listeners.add(fn) return () => this.listeners.delete(fn) } /** Format recent events as system-prompt context string. */ contextString(limit = 15, maxAgeMs = 5 * 60 * 1000): string { const recent = this.recent(limit, maxAgeMs) if (!recent.length) return '' const lines = ['### 🔔 Recent Events'] const icons: Record = { task: '📋', scheduler: '⏰', telegram: '📱', webhook: '🪝', tool: '🔧', ambient: '🌙', mesh: '🔗', error: '⚠️', } const kindIcons: Partial> = { started: '▶', completed: '✅', error: '❌', received: '←', sent: '→', fired: '🔥', info: 'ℹ', } for (const e of recent) { const t = new Date(e.timestamp).toLocaleTimeString() const si = icons[e.source] || '•' const ki = kindIcons[e.kind] || '' lines.push(`- [${t}] ${si}${ki} **${e.source}**: ${e.summary.slice(0, 140)}`) } // Summary counts const counts: Record = {} recent.forEach(e => { counts[e.source] = (counts[e.source] || 0) + 1 }) const summary = Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(', ') if (summary) lines.push(`*Event summary: ${summary}*`) return lines.join('\n') } } export const eventBus = new EventBus() /** Convenience: fire a bus event from anywhere. */ export function fireBusEvent(ev: Omit) { window.dispatchEvent(new CustomEvent('careless:bus', { detail: ev })) }