/** * Thread Status Queue Service * * Tracks the lifecycle of every inbound message thread across platforms * (Slack, GChat, Telegram, etc.). On orchestrator restart, non-terminal * threads are recovered and re-enqueued so no user messages are silently dropped. * * Persistence is debounced and written to ~/.crewly/thread-status-queue.json * using atomic file I/O to prevent corruption. * * @module services/messaging/thread-status-queue */ import type { ThreadStatus, ThreadStatusEntry, TrackInboundInput, ThreadStatusStats, ReplyStatus, ThreadStatusTombstone } from '../../types/thread-status.types.js'; /** * Minimal interface for SchedulerService to avoid circular imports. * Used to create follow-up checks when a thread is marked as replied_to_follow_up. */ export interface ISchedulerServiceLike { scheduleCheck(targetSession: string, minutes: number, message: string, type?: string, options?: { label?: string; }): string; cancelCheck(checkId: string): void; } /** * Minimal interface for EventBusService to avoid circular imports. * Uses unknown for eventType/filter to avoid importing event-bus types. */ export interface IEventBusServiceLike { subscribe(input: unknown): { id: string; }; unsubscribe(subscriptionId: string): boolean; } /** * Minimal interface for MessageQueueService to avoid circular imports. * Used during restart recovery to re-enqueue pending threads. */ export interface IMessageQueueServiceLike { enqueue(input: { content: string; conversationId: string; source: string; sourceMetadata?: Record; targetSession?: string; }): unknown; } /** * Minimal interface for checking agent working status. * Used during recovery to determine if delegated agents have completed. */ export interface IAgentStatusCheckerLike { getAgentWorkingStatus(sessionName: string): Promise<'idle' | 'in_progress' | 'unknown'>; } /** * Callback for sending platform-specific confirmation messages during recovery. * e.g., "I am back online. I see your message: {preview}. Let me handle it now." * * @param entry - The thread entry to confirm * @returns Promise resolving when the confirmation is sent */ export type RecoveryConfirmCallback = (entry: ThreadStatusEntry) => Promise; /** * Tracks active follow-up subscriptions so they can be cancelled * when the user replies before the scheduled check fires. */ interface FollowUpTracking { /** ID of the scheduled check */ scheduledCheckId: string; /** ID of the event bus subscription */ eventSubscriptionId: string; } /** * Service that maintains a persistent queue of thread statuses. * Constructed with a crewly home directory path for disk persistence. * * Lifecycle: enqueued → delivered → replied_* (terminal or waiting_actions) * * @example * ```typescript * const svc = new ThreadStatusQueueService('/home/user/.crewly'); * await svc.loadPersistedState(); * * const entry = svc.trackInbound({ * source: 'slack', * threadKey: 'C123:170743.001', * conversationId: 'conv-1', * messagePreview: 'Deploy the new feature', * }); * * svc.markDelivered('C123:170743.001'); * svc.markReplied('C123:170743.001', 'replied_completed'); * ``` */ export declare class ThreadStatusQueueService { /** Singleton instance */ private static instance; /** Logger for this service */ private logger; /** All tracked thread entries, keyed by threadKey for O(1) lookup */ private entries; /** * Compact records of terminal entries removed by cleanup, keyed by * threadKey. "No entry" alone cannot tell a closed thread from an * untracked one; the tombstone can (#757). */ private tombstones; /** Path to the persistence file, or null if init() has not been called */ private persistPath; /** Debounce timer for batching persistence writes */ private persistTimer; /** ISO timestamp of last cleanup run */ private lastCleanupAt; /** Scheduler service for creating follow-up checks */ private schedulerService; /** Event bus for subscribing to message:received events */ private eventBusService; /** Tracks active follow-up subscriptions keyed by threadKey */ private followUpTracking; /** * Creates a new ThreadStatusQueueService. * Use getInstance() for singleton access, or construct directly for testing. * * @param crewlyHome - Optional crewly home directory path for immediate initialization */ constructor(crewlyHome?: string); /** * Returns the singleton instance of ThreadStatusQueueService. * Call init() after this to set the persistence path. * * @returns The singleton instance */ static getInstance(): ThreadStatusQueueService; /** * Resets the singleton instance. For testing only. */ static resetInstance(): void; /** * Loads persisted state from disk. * If the file doesn't exist or is corrupted, starts with an empty queue. */ loadPersistedState(): Promise; /** * Records a new inbound thread message with status `enqueued`. * If a thread with the same threadKey already exists, the existing entry is returned. * * @param input - Inbound message details * @returns The created (or existing) ThreadStatusEntry */ trackInbound(input: TrackInboundInput): ThreadStatusEntry; /** * Transitions a thread from `enqueued` (or `error`) to `delivered`. * No-op if thread is already in a terminal or later status. * * @param threadKey - Platform-specific thread identifier * @throws Error if threadKey is not found */ markDelivered(threadKey: string): void; /** * Set the SchedulerService instance for creating follow-up checks. * Called during server initialization. * * @param service - The SchedulerService instance (or compatible interface) */ setSchedulerService(service: ISchedulerServiceLike): void; /** * Set the EventBusService instance for subscribing to message events. * Called during server initialization. * * @param service - The EventBusService instance (or compatible interface) */ setEventBusService(service: IEventBusServiceLike): void; /** * Transitions a thread to one of the replied_* statuses. * Sets the repliedAt timestamp. * * When status is `replied_to_follow_up`, automatically: * 1. Creates a scheduled check (default 5 min) to verify user replied * 2. Subscribes to message:received events for this thread's conversation * 3. On user reply: cancels the scheduled check and transitions to replied_completed * * @param threadKey - Platform-specific thread identifier * @param status - One of: replied_completed, replied_waiting_actions, replied_to_follow_up * @throws Error if threadKey is not found or status is not a valid reply status */ markReplied(threadKey: string, status: ReplyStatus): void; /** * Handles a user reply to a follow-up thread. * Cancels the scheduled check, removes the event subscription, * and transitions the thread to replied_completed (or re-enqueues). * * @param threadKey - Platform-specific thread identifier */ handleFollowUpReply(threadKey: string): void; /** * Returns the active follow-up tracking for a thread, if any. * Useful for testing and debugging. * * @param threadKey - Platform-specific thread identifier * @returns The follow-up tracking info, or null */ getFollowUpTracking(threadKey: string): FollowUpTracking | null; /** * Records that an agent was delegated work from this thread. * * @param threadKey - Platform-specific thread identifier * @param agentSession - Session name of the delegated agent * @throws Error if threadKey is not found */ addDelegatedAgent(threadKey: string, agentSession: string): void; /** * Transitions a `replied_waiting_actions` thread to `replied_completed` * when all delegated agents have finished their work. * * @param threadKey - Platform-specific thread identifier * @throws Error if threadKey is not found */ markDelegationsComplete(threadKey: string): void; /** * Returns all entries not in a terminal status. Used for restart recovery. * * @returns Array of non-terminal ThreadStatusEntry objects */ getPendingThreads(): ThreadStatusEntry[]; /** * Returns all entries matching the given status. * * @param status - The ThreadStatus to filter by * @returns Array of matching ThreadStatusEntry objects */ getByStatus(status: ThreadStatus): ThreadStatusEntry[]; /** * Retrieves a specific entry by threadKey. * * @param threadKey - Platform-specific thread identifier * @returns The entry, or null if not found */ get(threadKey: string): ThreadStatusEntry | null; /** * Find a thread entry by its conversationId. * * Useful when the caller has a conversationId (e.g. from chat history) * but not the threadKey. Scans all entries — O(n) but the set is small. * * @param conversationId - The conversation ID to search for * @returns The most recently updated matching entry, or null */ getByConversationId(conversationId: string): ThreadStatusEntry | null; /** * The tombstone of a closed thread whose entry cleanup removed. * * @param threadKey - Platform-specific thread identifier * @returns The tombstone, or null when none is kept (never tracked, still * live, or aged past TOMBSTONE_RETENTION_HOURS) */ getTombstone(threadKey: string): ThreadStatusTombstone | null; /** * The most recent tombstone for a conversation id. * * @param conversationId - Conversation id of the removed entry * @returns The tombstone, or null */ getTombstoneByConversationId(conversationId: string): ThreadStatusTombstone | null; /** * Expires entries that have been in a non-terminal status longer than maxAgeMinutes. * Transitions them to `expired`. * * @param maxAgeMinutes - Maximum age in minutes (defaults to STALE_TIMEOUT_MINUTES) * @returns Number of entries expired */ expireStale(maxAgeMinutes?: number): number; /** * Removes terminal entries older than retentionHours. * Also enforces MAX_ENTRIES by pruning oldest terminal entries first. * * @param retentionHours - Hours to retain terminal entries (defaults to CLEANUP_RETENTION_HOURS) * @returns Number of entries removed */ cleanup(retentionHours?: number): number; /** * Returns statistics for monitoring the thread status queue. * * @returns Stats snapshot including total, per-status counts, and oldest pending timestamp */ getStats(): ThreadStatusStats; /** * Initializes the service with a crewly home directory for persistence. * Must be called before loadPersistedState() when using getInstance(). * * @param crewlyHome - Absolute path to the crewly home directory (e.g. ~/.crewly) */ init(crewlyHome: string): void; /** * Public convenience method: optionally re-inits the persist path, * then loads persisted state from disk. * * @param crewlyHome - Optional crewly home directory to re-initialize with */ load(crewlyHome?: string): Promise; /** * Immediately flushes the current state to disk. Call during graceful shutdown * to ensure no in-memory mutations are lost. */ persist(): Promise; /** * Returns all entries in the queue. Used for the thread-status API. * * @returns Array of all ThreadStatusEntry objects */ getAllEntries(): ThreadStatusEntry[]; /** * Recovers pending threads after an orchestrator restart. * Loads persisted state and handles each thread based on its status: * * 1. **enqueued/delivered/error** — Send a "I'm back online" confirmation * via the platform, then re-enqueue for orchestrator processing. * 2. **replied_to_follow_up** — Re-setup follow-up tracking (scheduled * check + event subscription) to continue waiting for user reply. * 3. **replied_waiting_actions** — Check delegated agent statuses. If all * agents are done, transition to replied_completed. Otherwise keep waiting. * 4. **replied_completed/expired** — Already terminal, skip. * * @param messageQueueService - The message queue service for re-enqueuing * @param options - Optional callbacks for agent status checking and platform confirmations * @returns Summary of recovery actions taken */ recoverPendingThreads(messageQueueService: IMessageQueueServiceLike, options?: { agentStatusChecker?: IAgentStatusCheckerLike; confirmCallback?: RecoveryConfirmCallback; }): Promise<{ loaded: number; reEnqueued: number; followUpRestored: number; waitingActionsChecked: number; delegationsCompleted: number; expired: number; cleaned: number; }>; /** * Cleans up timers and follow-up tracking. Call on shutdown. */ destroy(): void; /** * Retrieves an entry by threadKey or throws an error with a descriptive message. * * @param threadKey - Platform-specific thread identifier * @param caller - Name of the calling method (for error messages) * @returns The found entry * @throws Error if entry is not found */ private getOrThrow; /** * Sets up follow-up tracking for a replied_to_follow_up thread. * Creates a scheduled check and an event subscription so that if * the user replies, the check is cancelled and the thread is completed. * * @param threadKey - Platform-specific thread identifier * @param entry - The thread status entry */ private setupFollowUpTracking; /** * Keep a compact record of a terminal entry cleanup is removing. * * @param entry - The entry being removed * @param removedAt - Removal time (ISO) */ private addTombstone; /** * Drop tombstones past TOMBSTONE_RETENTION_HOURS (by the entry's last * update), then the oldest beyond MAX_TOMBSTONES. * * @returns Number of tombstones dropped */ private pruneTombstones; /** * Schedules a debounced persistence write to disk. * Multiple mutations within PERSIST_DEBOUNCE_MS are batched into a single write. */ private schedulePersist; /** * Writes the current queue state to disk atomically. */ private persistToDisk; } export {}; //# sourceMappingURL=thread-status-queue.service.d.ts.map