/** * Event Bus Service * * Central pub/sub service for agent lifecycle events. Agents publish events * (idle, busy, active, inactive) and subscribers receive notifications when * matching events occur. Notifications are delivered via the MessageQueueService * to the orchestrator terminal. * * @module services/event-bus/event-bus */ import { EventEmitter } from 'events'; import type { MessageQueueService } from '../messaging/message-queue.service.js'; import type { SlackThreadStoreService } from '../slack/slack-thread-store.service.js'; import type { AgentEvent, EventSubscription, CreateSubscriptionInput, EventType } from '../../types/event-bus.types.js'; /** * In-process event handler signature used by {@link EventBusService.onInProcess}. * * Handlers may be sync or async. Async handlers run concurrently with the * publisher's synchronous control flow — `publish()` does NOT await pending * promises, but rejected promises are caught and logged, never propagated. */ export type InProcessEventHandler = (event: AgentEvent) => void | Promise; /** * Detach function returned by {@link EventBusService.onInProcess}. Invoke to * unregister the handler from the bus. Idempotent — calling more than once is * a no-op. */ export type InProcessUnsubscribe = () => void; /** * EventBusService manages event subscriptions and publishes agent lifecycle * events. When an event matches a subscription, a notification message is * enqueued into the MessageQueueService for delivery to the subscriber. * * @example * ```typescript * const eventBus = new EventBusService(); * eventBus.setMessageQueueService(queueService); * * eventBus.subscribe({ * eventType: 'agent:idle', * filter: { sessionName: 'agent-joe' }, * subscriberSession: 'crewly-orc', * oneShot: true, * }); * * eventBus.publish(agentIdleEvent); * ``` */ export declare class EventBusService extends EventEmitter { private logger; private subscriptions; private messageQueueService; private slackThreadStore; private cleanupInterval; private deliveryCount; /** * Buffered notifications pending delivery after debounce window. * Key: `${subscriberSession}:${event.sessionName}` for per-agent dedup. * When the same agent fires multiple events within the debounce window, * only the latest event is kept (deduplication by overwrite). */ private pendingNotifications; /** Debounce timer for batching event notifications */ private batchTimer; /** Hard-deadline timer that forces a flush even if debounce keeps resetting */ private maxWaitTimer; /** * Dedup guard for publish(): ignores duplicate events for the same * (type, sessionName) within EVENT_DEBOUNCE_WINDOW_MS (5s). * Key: `${eventType}:${sessionName}`, value: timestamp (ms). */ private recentPublishMap; /** * Event ids already delivered per subscription (#926), so a replayed * publish of the same event does not wake the subscriber twice. * Key: `${subscriptionId}||${eventId}`, value: delivery timestamp (ms). */ private deliveredEventIds; /** * In-process subscribers keyed by event type. Used by internal services * (EventToWorkItemBridge, LEARN-1's auto-record-learning subscriber) that * need to react to events synchronously without going through the * agent-session subscription queue. * * The agent-session subscriptions ({@link subscriptions}) and these * in-process handlers are intentionally separate — the former routes * through MessageQueueService and is rate-limited / debounced, the * latter fires immediately and is meant for trusted internal callers. */ private inProcessHandlers; constructor(); /** * Set the MessageQueueService instance for delivering notifications. * * @param service - The MessageQueueService instance */ setMessageQueueService(service: MessageQueueService): void; /** * Set the SlackThreadStoreService for enriching notifications with thread paths. * * @param store - The SlackThreadStoreService instance */ setSlackThreadStore(store: SlackThreadStoreService): void; /** * Create a new event subscription. * * @param input - Subscription configuration * @returns The created EventSubscription * @throws Error if input is invalid or limits are exceeded */ subscribe(input: CreateSubscriptionInput): EventSubscription; /** * Remove a subscription by ID. * * @param subscriptionId - ID of the subscription to remove * @returns True if the subscription was found and removed */ unsubscribe(subscriptionId: string): boolean; /** * Publish an event to all matching subscriptions. * For each match, formats a notification message and enqueues it * into the MessageQueueService for delivery to the subscriber. * * @param event - The agent lifecycle event to publish */ publish(event: AgentEvent): void; /** * Register an in-process handler for one or more event types. * * Used by trusted internal services (EventToWorkItemBridge, LEARN-1's * auto-record-learning subscriber) that need synchronous reactivity to * the event stream without going through the agent-session subscription * queue. These handlers fire from `publish()` immediately after the * `event_published` EventEmitter signal, before the debounced * notification flush, so callers can rely on the same publish ordering * semantics as `EventEmitter.on('event_published', …)` listeners. * * Handler errors are isolated: * - synchronous throws are caught and logged * - rejected promises returned by async handlers are caught and logged * - neither propagates to other in-process handlers nor to the * publisher's `publish()` call site * * Returned `unsubscribe` is idempotent. * * @param eventTypes - One event type or an array of event types to listen for * @param handler - Sync or async handler invoked once per matching event * @returns A function that detaches the handler when called * * @example * ```typescript * const unsubscribe = eventBus.onInProcess( * ['task:done_by_worker', 'task:rejected'], * async (event) => { * await bridge.handleTaskEvent(event); * }, * ); * // Later: unsubscribe(); * ``` */ onInProcess(eventTypes: EventType | EventType[], handler: InProcessEventHandler): InProcessUnsubscribe; /** * Invoke every in-process handler registered for an event's type. * Sync throws and async rejections are caught + logged; never propagated. * * @param event - The event being published */ private dispatchInProcess; /** * List subscriptions, optionally filtered by subscriber session. * * @param subscriberSession - Optional session name to filter by * @returns Array of matching subscriptions */ listSubscriptions(subscriberSession?: string): EventSubscription[]; /** * Get a specific subscription by ID. * * @param subscriptionId - ID of the subscription * @returns The subscription or undefined */ getSubscription(subscriptionId: string): EventSubscription | undefined; /** * Get event bus statistics. * * @returns Stats object with subscription and delivery counts */ getStats(): { subscriptionCount: number; deliveryCount: number; }; /** * Stop the cleanup timer and clear all subscriptions. */ cleanup(): void; /** * Buffer a notification for debounced delivery with per-agent deduplication. * Critical events (task:completed, task:failed, agent:inactive, etc.) bypass * the debounce buffer and are delivered immediately to avoid notification delay. * * @param sub - The subscription whose subscriber should receive the message * @param message - The formatted notification message * @param event - The original agent event */ private bufferNotification; /** * Deliver a notification immediately without debouncing. * Used for critical events that must reach the subscriber ASAP. * * @param subscriberSession - The session to deliver to * @param message - The formatted notification message */ private deliverImmediately; /** * Flush all buffered notifications as a single combined message. * Groups by subscriber session and delivers one enqueue per subscriber. */ private flushNotifications; /** * Check if an event matches a subscription's type and filter criteria. * * @param event - The event to check * @param sub - The subscription to match against * @returns True if the event matches the subscription */ private matchesSubscription; /** * Record that `eventId` was delivered to `subscriptionId`, returning true * when it already was within {@link EVENT_BUS_CONSTANTS.DELIVERED_EVENT_TTL_MS}. * Entries older than the TTL are pruned once the map grows past * {@link EVENT_BUS_CONSTANTS.DEDUP_MAP_CLEANUP_THRESHOLD}. * * @param subscriptionId - The matching subscription * @param eventId - The published event's id * @param nowMs - Current time in ms * @returns True when this (subscription, event id) pair was seen recently */ private wasDeliveredRecently; /** * Format a notification message for an event and subscription. * * @param event - The event that triggered the notification * @param sub - The subscription that matched * @returns Formatted notification message string */ private formatNotification; /** * Get the number of subscriptions for a given subscriber session. * * @param subscriberSession - Session name to count * @returns Number of subscriptions */ private getSubscriptionCountForSession; /** * Start the periodic cleanup timer to remove expired subscriptions. */ private startCleanup; /** * Remove all expired subscriptions. */ private cleanupExpired; } /** * Notification body for a WorkItem-level system event (`task:verified`, * `task:done`, …), whose envelope has no agent session or team (#926). * Names the item, its title and the worker session instead of rendering * `Agent "" (session: )`. * * @param event - A system event that carries `workItemId` * @returns The message body, without the `[EVENT:…]` prefix */ export declare function formatWorkItemEventBody(event: AgentEvent): string; //# sourceMappingURL=event-bus.service.d.ts.map