/** * Chat Service — facade over ChatV2Service. * * Phase 6 of the unified-chat-message-store spec * (`specs/2026-05-14-unified-chat-message-store.md`): * the legacy JSON-file storage at `~/.crewly/chat/` is retired. * All chat persistence and read paths now flow through the canonical * `ChatV2Service` (SQLite at `~/.crewly/chat.db`). This file remains * temporarily as a deprecation shim so the many legacy callers * (`chat.controller.ts`, `chat.gateway.ts`, `slack-orchestrator-bridge.ts`, * `notify-reconciliation.service.ts`, `message-replay.service.ts`, * `index.ts`) continue to compile and run while they are migrated to * call ChatV2Service directly. Each facade method translates between * the legacy `ChatMessage` / `ChatConversation` DTOs and chat-v2's * `ChatMessageDTO` / `ChatChannelDTO` so existing event subscribers * (chat.gateway.ts) see the same shapes they always have. * * @module services/chat/chat.service */ import { EventEmitter } from 'events'; import { ChatMessage, ChatConversation, ChatSender, ChatChannelType, SendMessageInput, SendMessageResult, ChatMessageFilter, ConversationFilter } from '../../types/chat.types.js'; export declare class ConversationNotFoundError extends Error { readonly conversationId: string; constructor(conversationId: string); } export declare class MessageValidationError extends Error { constructor(message: string); } export interface ChatServiceOptions { /** @deprecated Legacy filesystem path; ignored — storage is now SQLite. */ chatDir?: string; } /** * Deprecated façade over ChatV2Service. * * Preserves the public surface of the original `ChatService` so the * remaining legacy callers compile unchanged. Internally every method * delegates to `ChatV2Service`. The original ~/.crewly/chat/*.json * storage layer has been removed. * * New code MUST NOT depend on this class; call `getChatV2Service()` * directly. Phase 6c of the spec deletes this file once all callers * are migrated. */ export declare class ChatService extends EventEmitter { private logger; private chatV2; constructor(_options?: ChatServiceOptions); initialize(): Promise; isInitialized(): boolean; /** * Send a user message. Idempotency via legacy callers' own * metadata.clientMessageId when present. */ sendMessage(input: SendMessageInput): Promise; /** * Add an agent reply extracted from raw terminal output. The legacy * regex extraction (`[RESPONSE]` / `[CHAT_RESPONSE]` markers) is gone * — callers should pass already-clean content. Kept for source * compatibility with `chat.gateway.processTerminalOutput`, which has * no production callers post Phase 4 discovery. */ addAgentMessage(conversationId: string, rawOutput: string, sender: ChatSender, metadata?: Record): Promise; /** * Persist a pre-extracted markdown reply. Primary call site is the * PTY `[NOTIFY]` path in `chat.gateway.processNotifyMessage`. */ addDirectMessage(conversationId: string, content: string, sender: ChatSender, metadata?: Record): Promise; /** * Add a server-side system note (progress markers, errors, etc.). */ addSystemMessage(conversationId: string, content: string, metadata?: Record): Promise; private recordViaFacade; /** * Read messages from a conversation with the legacy filter contract. * * Semantics (matching the original JSON-backed service, issue #1000): * - Default (no `offset`): the NEWEST `limit` messages that match every * filter, returned oldest → newest. `before` / `after` bound the window * exclusively, so `before=` yields the * `limit` messages immediately preceding that point ("load older"), * and `after=` yields the newest `limit` messages after it (the * original service also returned the newest tail when `after` was set, * which is what `ChatHighlightsService` relies on). * - With `offset`: legacy offset pagination over the matching window, * counted from the OLDEST message — skip `offset`, return `limit`. * * `before` / `after` accept a message id from this conversation (exact * seq boundary) or an ISO timestamp (string comparison, legacy). * * Implementation: walks chat-v2 `listMessages` pages through its cursors * (`backward` from the newest message or the `before` id; `forward` for * offset reads) and applies the content filters while walking, fetching * more pages until `limit` matches are found, the window boundary is * crossed, or the channel is exhausted. So a sparse filter does not * return fewer than `limit` messages while older matches exist — up to * the {@link LEGACY_MAX_SCAN_ROWS} scan bound. Stopping the walk at the * opposite boundary assumes chat-v2 `seq` order agrees with `createdAt` * order (both are assigned at insert time from the same clock). * * @param filter - Legacy message filter (`conversationId` required) * @returns Matching messages in chronological order; [] for an unknown * conversation or when `conversationId` is missing * @throws Any chat-v2 error other than "channel not found" * * @example * ```typescript * const newest = await chatService.getMessages({ conversationId, limit: 50 }); * const older = await chatService.getMessages({ conversationId, limit: 50, before: newest[0].id }); * ``` */ getMessages(filter: ChatMessageFilter): Promise; /** * Count messages in a conversation, honoring the same filters as * {@link getMessages} (`limit` / `offset` are ignored). Returns 0 for an * unknown conversation (legacy contract) instead of surfacing chat-v2's * 404. Unfiltered counts use chat-v2's count query; filtered counts walk * the same pages as `getMessages`, bounded by {@link LEGACY_MAX_SCAN_ROWS}. * * @param filter - Legacy message filter (`conversationId` required for a non-zero count) * @returns Number of matching messages * @throws Any chat-v2 error other than "channel not found" */ getMessageCount(filter: ChatMessageFilter): Promise; /** * Resolve a legacy `before` / `after` value to a boundary. A value that * is the id of a message in this conversation becomes an exact `seq` * boundary; anything else is treated as a timestamp. * * @param conversationId - Conversation being read * @param value - Raw `before` / `after` value * @returns The anchor, or null when no value was supplied */ private resolveLegacyAnchor; /** * Resolve both window boundaries of a legacy filter. * * @param conversationId - Conversation being read * @param filter - Legacy message filter * @returns The resolved window */ private resolveLegacyWindow; /** * Walk a channel's messages through chat-v2 cursor pagination, one full * chat-v2 page (`MessageStore.MAX_LIMIT`) at a time. Pages are fetched * lazily, so a consumer that stops early never loads further pages. * * @param conversationId - Channel to walk * @param direction - `backward` = newest → oldest, `forward` = oldest → newest * @param startSeq - Exclusive starting seq, or null to start at the channel * end (newest for backward, first message for forward) * @returns A generator of chat-v2 DTOs in walk order, at most * {@link LEGACY_MAX_SCAN_ROWS} of them * @throws {ChatError} `channel_not_found` when the conversation does not exist */ private walkChannel; /** * Select the newest `limit` messages inside the window that match the * content filters, walking backward from the `before` id (or the newest * message) and stopping at the `after` boundary. * * @param conversationId - Conversation being read * @param filter - Legacy message filter (content filters) * @param window - Resolved before/after window * @param limit - Max messages to return (`Infinity` to collect all, for counts) * @returns Matching messages, oldest → newest */ private selectNewestInWindow; /** * Legacy offset pagination: skip the first `offset` matching messages of * the window (counted from the oldest) and return the next `limit`. * * @param conversationId - Conversation being read * @param filter - Legacy message filter (content filters) * @param window - Resolved before/after window * @param offset - Matching messages to skip, counted from the oldest * @param limit - Max messages to return * @returns Matching messages, oldest → newest */ private selectFromOffsetInWindow; /** * Look up a single message by id within a conversation. * * @param conversationId - Conversation the message must belong to * @param messageId - The message id * @returns The legacy message, or null when absent or in another conversation */ getMessage(conversationId: string, messageId: string): Promise; updateMessageMetadata(_conversationId: string, messageId: string, metadataPatch: Record): Promise; getMessagesWithPendingSlackDelivery(maxAgeMs: number): Promise; /** * List conversations in the legacy shape, honoring the legacy filters: * `includeArchived` (archived rows are hidden by default), `channelType`, * a case-insensitive title `search`, and `offset` / `limit` pagination. * * @param filter - Optional legacy conversation filter * @returns Matching conversations */ getConversations(filter?: ConversationFilter): Promise; getConversation(id: string): Promise; createNewConversation(title?: string, idOverride?: string, _channelType?: ChatChannelType): Promise; /** * Rename a conversation. * * @param id - Conversation id * @param title - New title * @returns The updated conversation * @throws {ConversationNotFoundError} when the conversation does not exist */ updateConversationTitle(id: string, title: string): Promise; /** * Archive a conversation. * * @param id - Conversation id * @throws {ConversationNotFoundError} when the conversation does not exist */ archiveConversation(id: string): Promise; /** * Unarchive a conversation. * * @param id - Conversation id * @throws {ConversationNotFoundError} when the conversation does not exist */ unarchiveConversation(id: string): Promise; /** * Delete a conversation and its messages. Idempotent: deleting an * unknown conversation is a no-op (legacy contract). * * @param id - Conversation id */ deleteConversation(id: string): Promise; clearConversation(id: string): Promise; getCurrentConversation(): Promise; emitTypingIndicator(conversationId: string, sender: ChatSender, isTyping: boolean): void; emitProgress(conversationId: string, text: string): void; getStatistics(): Promise<{ totalConversations: number; activeConversations: number; archivedConversations: number; totalMessages: number; }>; /** * Generate a conversationId for a brand-new sendMessage call that * didn't supply one. Web-chat-style id; Slack callers always pass * their own slack-CHANNEL-TS identifier. */ private synthesizeConversationId; /** * Emit the legacy `conversation_updated` event. chat-v2 has no * channel-touched event yet, so the façade remains its source and * `ChatGateway` forwards it to WebSocket clients (the chat sidebar * listens for it). * * @param conversation - The conversation in its new state */ private emitConversationUpdated; /** * Re-read a conversation and emit `conversation_updated` for it. * No-op when the conversation no longer exists. * * @param id - Conversation id */ private emitConversationUpdatedById; /** * Run a chat-v2 mutation on a conversation, translating chat-v2's * `channel_not_found` error into the legacy `ConversationNotFoundError` * that callers (chat.controller → 404) catch by type. * * @param id - Conversation id * @param fn - The chat-v2 operation * @returns Whatever `fn` returns * @throws {ConversationNotFoundError} when the conversation does not exist */ private withConversation; /** * @deprecated Will be removed when the façade is retired. */ ensureInitialized(): Promise; } export declare function getChatService(options?: ChatServiceOptions): ChatService; export declare function resetChatService(): void; //# sourceMappingURL=chat.service.d.ts.map