import type { PlatformAdapter } from '../adapter.js'; import type { MessageRef, MessageContent, MessageContext, MessageEditContext, ActionContext, ModalSubmitContext, ModalDefinition, PlatformCapabilities, PlatformFileRef, DownloadedFile, Destination, PostMessageOpts, FileUploadOpts, ActionElement } from '../types.js'; import { TokenBucketRateLimiter } from '../utils/rate-limiter.js'; import type { OutputStream, OpenOutputStreamOpts } from '../output-stream.js'; export interface SlackAdapterConfig { botToken: string; signingSecret: string; appToken: string; adminChannel?: string; } export declare class SlackAdapter implements PlatformAdapter { readonly name = "slack"; readonly capabilities: PlatformCapabilities; private app; private client; private config; private editHandler; private _adminAutoDetected; /** * Per-message pending edit buffer for chat.update coalescing. * When multiple callers race to update the same message, only the latest * content is preserved — intermediate edits are discarded. The async flush * loop acquires a rate-limit token, then sends whatever the latest content * is at that point. * * Key = `${channel}:${ts}`, value = latest content + shared promise. * Bounded at PENDING_EDITS_MAX entries with LRU-style eviction. */ private pendingEdits; /** Token-bucket rate limiter shared across all Slack API methods. */ private rateLimiter; constructor(config: SlackAdapterConfig); private _envNum; private static readonly PREFIX; /** Add the `slack:` prefix (idempotent; passes through empty strings). */ private _wrap; /** Strip the `slack:` prefix (tolerates already-bare values for back-compat). */ private _unwrap; /** True if this conduit belongs to the Slack adapter. */ ownsConduit(conduit: string): boolean; start(): Promise; stop(): Promise; onMessage(handler: (ctx: MessageContext) => Promise): void; onMessageEdit(handler: (ctx: MessageEditContext) => Promise): void; onAction(actionId: string, handler: (ctx: ActionContext) => Promise): void; onModalSubmit(callbackId: string, handler: (ctx: ModalSubmitContext) => Promise): void; /** * Wrap a Slack API call with rate-limiting + 429 response handling. * 1. Acquires a token from the rate limiter (blocks if needed) * 2. Calls the API * 3. On 429: reports backpressure to the rate limiter, re-throws */ private rateLimitedCall; postMessage(destination: Destination, content: MessageContent, opts?: PostMessageOpts): Promise; /** * Update a message with per-message coalescing. * * If another update is already in-flight for the same message (pending a rate * limiter token), this call replaces the buffered content and returns the * shared promise — the intermediate edit is discarded. Once the rate limiter * grants a token, the latest content is sent via chat.update. * * This eliminates wasted API calls when multiple callers (OutputStream * streaming, status helpers, lifecycle) concurrently update the same message * while rate-limited. * * Internal retry loop: on 429, re-queues the entry and loops back to acquire * instead of recursing, which avoids coalescing confusion. */ updateMessage(ref: MessageRef, content: MessageContent): Promise; /** * Evict the oldest entry if the pendingEdits map exceeds PENDING_EDITS_MAX. * Resolves the evicted promise so any waiting callers don't hang. */ private _evictStalePending; deleteMessage(ref: MessageRef): Promise; postInteractive(destination: Destination, content: MessageContent & { actions: ActionElement[]; }, opts?: PostMessageOpts): Promise; openModal(triggerId: string, modal: ModalDefinition): Promise; private _addHourglassReaction; markQueued(ref: MessageRef): Promise; uploadFile(destination: Destination, filePath: string, opts?: FileUploadOpts): Promise; downloadFile(fileRef: PlatformFileRef, destDir: string): Promise; getPermalink(ref: MessageRef): Promise; /** Write CORTEX_ADMIN_CHANNEL to the .env file for persistence across restarts. */ private _persistAdminChannel; /** Expose the rate limiter for sharing with MCP tools and testing. */ getRateLimiter(): TokenBucketRateLimiter; openOutputStream(destination: Destination, opts?: OpenOutputStreamOpts): OutputStream; bindProjectConduit(projectId: string, conduitHint: string): Promise; unbindProjectConduit(projectId: string): Promise; getProjectConduits(): Promise>; resolveInboundProject(conduit: string): Promise; private _conduitsStore; private _getConduitsStore; /** * Resolve a Destination to a concrete Slack channel + kind label. * Returns channel=null for destinations that should be silently dropped * (unregistered project, unconfigured admin channel). */ private resolveDestination; private normalizeModalValues; private richBlocksToSlack; private actionElementToSlack; private modalToSlack; private resolveFilePath; }