/** * Queue Processor Service * * Processes messages from the queue one-at-a-time. Dequeues the next message, * delivers it to the orchestrator via AgentRegistrationService using a * fire-and-forget pattern. Responses are handled asynchronously by the * orchestrator through reply-* skills (reply-slack, reply-chat, reply-gchat). * * #239: Force-delivered user messages (Slack/web chat) are tracked in a * pending-ack list. When the agent returns to prompt, the PTY output is * checked for evidence the message was processed. If not found, the message * is re-delivered to prevent silent loss. * * @module services/messaging/queue-processor */ import { EventEmitter } from 'events'; import { MessageQueueService } from './message-queue.service.js'; import { ResponseRouterService } from './response-router.service.js'; import { AgentRegistrationService } from '../agent/agent-registration.service.js'; import { type RuntimeType } from '../../constants.js'; import type { ThreadStatusQueueService } from './thread-status-queue.service.js'; import type { SourceMetadata } from '../../types/messaging.types.js'; /** * Entry in the pending-ack list for force-delivered user messages. * Tracks all information needed to verify or redeliver the message. */ export interface PendingAckEntry { /** Unique message ID */ messageId: string; /** Conversation ID used as a fingerprint to detect processing in PTY output */ conversationId: string; /** The formatted delivery content (for redelivery) */ deliveryContent: string; /** Target session the message was delivered to */ targetSession: string; /** Runtime type of the target agent */ runtimeType: RuntimeType; /** Timestamp when force-delivered */ deliveredAt: number; /** Number of redelivery attempts so far */ retryCount: number; /** The original queued message (for markCompleted / markFailed / routeResponse) */ originalMessage: import('../../types/messaging.types.js').QueuedMessage; } /** * Keep only the JSON-safe scalar fields of a message's source metadata * (channelId, threadTs, userId, …). Callbacks such as `slackResolve` cannot * outlive the process and are dropped. * * @param metadata - Source metadata of a queued message * @returns Scalar fields, or undefined when there are none */ export declare function serializableSourceMetadata(metadata: Record | undefined): Record | undefined; export declare class QueueProcessorService extends EventEmitter { private logger; private queueService; private responseRouter; private agentRegistrationService; private running; private processing; private processNextTimeout; /** Set to true when an early-return path has already scheduled the next run. */ private nextAlreadyScheduled; /** * Tracks message IDs that were successfully delivered to an agent. * Prevents duplicate delivery when the queue requeues a message that * the agent has already received and started processing. * Entries are purged after DEDUP_TTL_MS to prevent memory leaks. */ private deliveredMessageIds; /** Timer for periodic cleanup of stale dedup entries */ private dedupCleanupTimer; /** Timer for periodic cleanup of stale pending-ack entries */ private pendingAckCleanupTimer; /** * #239: Tracks force-delivered user messages awaiting acknowledgment. * When a user message is force-delivered (agent not at prompt), it is * added here instead of being marked completed. On the next successful * waitForAgentReady (agent returns to prompt), the PTY output is checked * for evidence that each pending message was actually processed. */ private pendingAckMessages; /** Thread status queue for tracking message lifecycle across restarts */ private threadStatusQueue; /** * Optional Task Pool routing callback. When set, task-assignment messages * (content starting with "[TASK]") are routed through the Task Pool before * delivery, allowing the Reconciler to manage assignment lifecycle. */ private taskPoolRouter; constructor(queueService: MessageQueueService, responseRouter: ResponseRouterService, agentRegistrationService: AgentRegistrationService); /** * Set the thread status queue service for tracking message delivery lifecycle. * * @param service - The ThreadStatusQueueService instance */ setThreadStatusQueue(service: ThreadStatusQueueService): void; /** * Set the Task Pool routing callback. * When provided, messages with "[TASK]" prefix are offered to the Task Pool * before standard delivery. If the callback returns true, the message is * considered routed and delivery is skipped. * * @param router - Async callback that returns true if the message was routed to the pool */ setTaskPoolRouter(router: (messageContent: string, targetSession: string) => Promise): void; /** * Start the processor. Listens to queue 'enqueued' events and * triggers processing. */ start(): void; /** * Stop the processor. Clears timers and removes listeners. */ stop(): void; /** * Check if the processor is currently running. * * @returns True if running */ isRunning(): boolean; /** * Check if a message is currently being processed. * * @returns True if processing */ isProcessingMessage(): boolean; /** * Handler for queue 'enqueued' events. Triggers processing if idle. */ private onMessageEnqueued; /** * Schedule the next message processing after a delay. * * @param delayMs - Delay in milliseconds before processing */ private scheduleProcessNext; /** * Process the next message in the queue. * This is the core processing loop. */ private processNext; /** * Schedule processing of the next message if there are pending messages. * Uses INTER_MESSAGE_DELAY to avoid overwhelming the orchestrator. * * #218: Skip the inter-message delay when a user message is pending. * This ensures user messages (from Slack, web chat, etc.) are never * blocked behind the 500ms cooldown after system event delivery. */ private scheduleNextIfPending; /** * Start periodic cleanup of stale entries in the delivered message dedup set. * Runs every DEDUP_TTL_MS and removes entries older than DEDUP_TTL_MS. */ private startDedupCleanup; /** * Stop the periodic dedup cleanup timer. */ private stopDedupCleanup; /** * Remove entries from the dedup set that are older than DEDUP_TTL_MS. */ private purgeStaleDedup; /** * Get the number of tracked delivered message IDs (for testing). * * @returns Number of entries in the dedup set */ getDeliveredMessageCount(): number; /** * Get the number of messages awaiting acknowledgment (for testing). * * @returns Number of entries in the pending-ack list */ getPendingAckCount(): number; /** * Start periodic cleanup of stale pending-ack entries. * Runs at PENDING_ACK_TTL_MS intervals and fails entries that have exceeded * the TTL — prevents memory leaks if an agent crashes and never returns to prompt. */ private startPendingAckCleanup; /** * Stop the periodic pending-ack cleanup timer. */ private stopPendingAckCleanup; /** * Remove pending-ack entries that have exceeded the TTL. * Marks them as failed and routes error to the source so the user is notified. */ private purgeExpiredPendingAcks; /** * Check if a pending-ack message's fingerprint appears in PTY output. * * Matches the full [CHAT:conversationId:fingerprint] or [GCHAT:conversationId:fingerprint] * prefix. The fingerprint (last 8 chars of messageId) distinguishes between different * messages in the same conversation/thread, preventing false-positive ack confirmations * when a user sends multiple messages in quick succession. * * Falls back to conversationId-only matching for messages delivered before the * fingerprint format was introduced (backward compat). * * @param output - Captured PTY output string * @param conversationId - The conversation ID to search for * @param messageId - The unique message ID (last 8 chars used as fingerprint) * @returns true if the message fingerprint was found */ private isPendingAckInOutput; /** * #239: Flush pending-ack messages by verifying them against PTY output. * * Called when the agent returns to prompt (waitForAgentReady succeeds). * For each pending-ack entry: * - Captures the agent's recent PTY output * - Checks if the conversationId or CHAT prefix appears in the output * - If found → message was processed, mark completed * - If not found → message was swallowed, redeliver * - If max retries exceeded → mark failed * * @param sessionName - The agent session to check * @param runtimeType - Runtime type for redelivery */ private flushPendingAcks; } /** * Record whom a queued message's turn acts for (issue #968): * * - a message an agent wrote (`authorAgentSession`, also on system events * that relay an agent's status): that agent's person (unchanged when the * agent has no record here); * - a Slack message: its sender (never a bot); * - a system event (scheduler check, autonomous run, reconciler or other * system notice): the owner — not whoever spoke last; * - the dashboard and the owner's own channels: the owner. * * Machine-to-machine messages change nothing. * * @param targetSession - Receiving session * @param source - Message source * @param metadata - Source metadata (`actingForUserId` = the message's own sender) */ export declare function noteQueuedActingFor(targetSession: string, source: string | undefined, metadata: SourceMetadata | undefined): void; //# sourceMappingURL=queue-processor.service.d.ts.map