/** * Message Queue Service * * FIFO queue for sequential message processing with optional disk persistence. * All chat and Slack messages are enqueued here, processed one-at-a-time, * and responses are routed back to the correct source. * * When an crewlyHome path is provided, the queue state is persisted to * `~/.crewly/queue/message-queue.json` after every mutation so that * pending messages survive backend restarts. * * @module services/messaging/message-queue */ import { EventEmitter } from 'events'; import type { QueuedMessage, EnqueueMessageInput, QueueStatus } from '../../types/messaging.types.js'; /** * MessageQueueService manages a FIFO queue of messages destined for * the orchestrator. It provides enqueue/dequeue operations, status * tracking, and EventEmitter-based notifications for queue state changes. * * When constructed with an crewlyHome path, the queue is persisted to * disk after every state change and restored on startup via loadPersistedState(). * * @example * ```typescript * // Without persistence (tests, legacy) * const queue = new MessageQueueService(); * * // With persistence * const queue = new MessageQueueService('~/.crewly'); * await queue.loadPersistedState(); * ``` */ export declare class MessageQueueService extends EventEmitter { /** Logger for this service */ private logger; /** Pending messages waiting to be processed (FIFO) */ private queue; /** Completed/failed message history (most recent first) */ private history; /** Total messages processed since startup */ private totalProcessed; /** Total messages that failed since startup */ private totalFailed; /** The message currently being processed */ private currentMessage; /** Path to the persistence directory, or null if persistence is disabled */ private persistDir; /** Full path to the persistence file */ private persistPath; /** * Set while the persisted file is bad (invalid, or not a queue) and could * not be copied aside. The next persist copies it aside first and refuses * to write while that fails. */ private persistBlockedReason; /** The persisted file could not be read (EMFILE, EIO…); persists re-read it first. */ private persistUnread; /** Debounce timer for batching persistence writes */ private persistTimer; /** Promise for the currently in-flight persistence write */ private persistPromise; /** * Create a new MessageQueueService. * * @param crewlyHome - Path to the crewly home directory (e.g. ~/.crewly). * When provided, enables disk persistence. When omitted, the queue is in-memory only. */ constructor(crewlyHome?: string); /** * Load persisted queue state from disk on startup. * If the file is missing, corrupt, or has the wrong version, starts empty. * In-flight messages (status === 'processing') are reset to pending and * placed at the front of the queue. System event messages are filtered out * since they are ephemeral and stale after restart. */ loadPersistedState(): Promise; /** * Read the persisted state and merge it into the live queue (at startup * the live queue is empty, so this restores it). * * - invalid JSON: copied aside by safeReadJson (`.corrupt-`), nothing restored; * - valid JSON of an unknown shape: copied aside, nothing restored; * - a read error (EMFILE, EIO…): the file may be good — marked unread, and * the next persist reads it again before it writes; * - a bad file whose copy failed: the next persist copies it aside first. * * @returns False while the file cannot be read */ private readAndApplyPersisted; /** * Flush any pending persistence write immediately. * Call this during shutdown to ensure the latest state is saved. */ flushPersist(): Promise; /** * Enqueue a new message for processing. * * @param input - The message to enqueue * @returns The created QueuedMessage * @throws Error if input is invalid or queue is full */ enqueue(input: EnqueueMessageInput): QueuedMessage; /** * Dequeue the next pending message for processing. * Prioritizes user messages (slack, web_chat, whatsapp, google_chat, telegram) over system events so that * real user conversations are never blocked behind internal notifications. * Returns null if the queue is empty. * * @returns The next QueuedMessage or null */ dequeue(): QueuedMessage | null; /** * Dequeue additional pending system_event messages (up to maxCount). * Used by the queue processor to batch multiple system events into a * single delivery, reducing context consumption on the orchestrator. * * Does NOT set currentMessage — the caller is responsible for marking * all returned messages as completed/failed. * * @param maxCount - Maximum additional system events to dequeue * @param targetSession - Only events for this recipient (undefined = the orchestrator) * @returns Array of dequeued system event messages */ dequeueSystemEventBatch(maxCount: number, targetSession?: string): QueuedMessage[]; /** * Re-queue a message that was dequeued but could not be delivered. * Resets the message status to pending and places it back at the front * of the queue so it will be the next message processed. * * @param message - The message to re-queue */ requeue(message: QueuedMessage): void; /** * Mark a message as completed with optional response. * * @param messageId - ID of the message to mark complete * @param response - Optional response content from orchestrator */ markCompleted(messageId: string, response?: string): void; /** * Mark a batch of messages as completed. * Used for system event batching where multiple messages are dequeued * via dequeueSystemEventBatch() and delivered together. * * @param messages - Array of messages to mark as completed */ markBatchCompleted(messages: QueuedMessage[]): void; /** * Mark a batch of messages as failed. * Used for system event batching when delivery of the combined batch fails. * * @param messages - Array of messages to mark as failed * @param error - Error message describing the failure */ markBatchFailed(messages: QueuedMessage[], error: string): void; /** * Mark a message as failed with an error. * * @param messageId - ID of the message to mark as failed * @param error - Error message describing the failure */ markFailed(messageId: string, error: string): void; /** * Cancel a pending message by ID. * Only messages with status 'pending' can be cancelled. * * @param messageId - ID of the message to cancel * @returns True if the message was found and cancelled */ cancel(messageId: string): boolean; /** * Force-cancel the currently processing message. * Use this to unstick a message that is stuck in 'processing' state. * * @returns True if there was a processing message to cancel */ forceCancelCurrent(): boolean; /** * Clear all pending messages from the queue. * * @returns Number of messages cleared */ clearPending(): number; /** * Get the current queue status summary. * * @returns QueueStatus object */ getStatus(): QueueStatus; /** * Get all pending messages in the queue (read-only copies). * * @returns Array of pending QueuedMessage objects */ getPendingMessages(): QueuedMessage[]; /** * Get the message history (completed/failed/cancelled). * * @returns Array of historical QueuedMessage objects (most recent first) */ getHistory(): QueuedMessage[]; /** * Get a specific message by ID from queue, current, or history. * * @param messageId - ID of the message to find * @returns The QueuedMessage or undefined */ getMessage(messageId: string): QueuedMessage | undefined; /** * Check if the queue has pending messages. * * @returns True if there are pending messages */ hasPending(): boolean; /** * Check if a user message (slack, web_chat, whatsapp, google_chat, telegram) * is pending in the queue. Used by the queue processor to skip inter-message * delay when user messages are waiting behind system events (#218). * * @returns True if a user message is pending */ hasUserMessagePending(): boolean; /** * Check if a message is currently being processed. * * @returns True if processing */ isProcessing(): boolean; /** * Get the number of pending messages. * * @returns Pending message count */ get pendingCount(): number; /** * Add a completed/failed message to history, maintaining max size. * * @param message - Message to add to history */ private addToHistory; /** * Emit a status update event with the current queue status. */ private emitStatusUpdate; /** * Schedule a debounced persistence write. Uses a 50ms debounce to batch * multiple rapid mutations into a single disk write. * No-op when persistence is disabled. */ private schedulePersist; /** * Write current queue state to disk using atomic temp-file-then-rename. * Errors are caught and logged to avoid disrupting queue operations. */ private persistState; } //# sourceMappingURL=message-queue.service.d.ts.map