import { type AgentMessageSender, type AgentPrecedingMessage } from "@mono-agent/agent-contracts"; import type { SlackConversationMessage } from "./types.js"; /** * Messages of context sent per turn. Matches Slack's 15-object ceiling for * non-Marketplace apps so the default works everywhere; internal apps can raise it. */ export declare const SLACK_THREAD_CONTEXT_DEFAULT_MAX_MESSAGES = 15; /** Objects requested per call. Same 15-object reasoning as the message default. */ export declare const SLACK_THREAD_CONTEXT_DEFAULT_REQUEST_LIMIT = 15; /** * Budget for the whole context phase. Deliberately far below the Slack client's * own 45s request timeout: this work sits between the 👀 reaction and the run, so * a slow read must be abandoned rather than waited on. */ export declare const SLACK_THREAD_CONTEXT_DEFAULT_TIMEOUT_MS = 4000; /** Fallback cooldown when a rate-limited response carries no `Retry-After`. */ export declare const SLACK_THREAD_CONTEXT_RATE_LIMIT_COOLDOWN_MS = 60000; /** Sending more than the harness renders is pointless, so this is the hard ceiling. */ export declare const SLACK_THREAD_CONTEXT_MAX_MESSAGES_CEILING = 30; /** Upper bound accepted for `requestLimit`; Slack's own maximum for internal apps. */ export declare const SLACK_THREAD_CONTEXT_REQUEST_LIMIT_CEILING = 1000; /** Tunes the best-effort thread/channel context read. */ export interface SlackThreadContextOptions { /** Default `true`. */ readonly enabled?: boolean; /** Messages sent per turn; clamped to 0..30. `0` disables the read. */ readonly maxMessages?: number; /** Objects requested from Slack; clamped to 1..1000. */ readonly requestLimit?: number; /** Budget for the whole phase (fetch + name resolution). `0` means no extra deadline. */ readonly timeoutMs?: number; /** Include OTHER apps' messages, labelled `isBot`. Default `true`; our own are always excluded. */ readonly includeBotMessages?: boolean; } /** Every resolved knob, with defaults applied and bounds enforced. */ export interface ResolvedSlackThreadContext { readonly enabled: boolean; readonly maxMessages: number; readonly requestLimit: number; readonly timeoutMs: number; readonly includeBotMessages: boolean; } /** Why a turn carried no transcript. Logged as a count; never with content. */ export type SlackThreadContextSkipReason = "disabled" | "unsupported_client" | "window_missed" | "rate_limited" | "missing_scope" | "not_in_channel" | "timeout" | "api_error" | "empty"; export declare function resolveSlackThreadContext(options: SlackThreadContextOptions | undefined): ResolvedSlackThreadContext; /** * The newest usable messages strictly preceding the trigger, oldest first. * * Pure: no clock, no network. Makes NO assumption about the order Slack returned * (`conversations.history` is newest-first, `conversations.replies` oldest-first, * and the latter always injects the thread parent), so everything is sorted * locally by numeric timestamp. * * `requireTrigger` is the window check for the replies path. Slack's docs do not * say which end `limit` truncates, so instead of trusting either reading we ask * for a page anchored at the trigger and verify the trigger came back. If it did * not, Slack gave us an unrelated slice of the thread and the only safe answer is * no transcript at all — a misleading one is worse than none. */ export declare function selectPrecedingSlackMessages(input: { readonly raw: readonly SlackConversationMessage[]; readonly triggerTs: string; readonly maxMessages: number; readonly requireTrigger: boolean; readonly ownBotUserIds: ReadonlySet; readonly ownBotId?: string; readonly includeBotMessages: boolean; }): { readonly kept: readonly SlackConversationMessage[]; readonly windowMissed: boolean; }; /** * One contract entry, or `undefined` when nothing usable survives. * * The body is normalized the same way inbound turn text is, EXCEPT that the * bot's own mention is left in place: a transcript should show that someone * pinged the agent. Reserved harness markup is deliberately NOT neutralized here * — the harness owns that, and escaping twice would corrupt legitimate text. */ export declare function toAgentPrecedingMessage(message: SlackConversationMessage, sender: AgentMessageSender | undefined): AgentPrecedingMessage | undefined; /** * Keeps the newest entries that fit `maxTotalBytes`, mirroring the harness's own * accumulation. Pre-trimming here is what keeps the `turn_context` counts honest: * the harness would silently drop the overflow either way. */ export declare function trimPrecedingToTotalBytes(entries: readonly T[], maxTotalBytes?: number): readonly T[]; /** * A Slack `seconds.micros` timestamp as canonical ISO-8601, or `undefined`. * * The harness echoes a timestamp only when it round-trips exactly * (`new Date(Date.parse(v)).toISOString() === v`), so anything this cannot * represent is dropped rather than guessed at — including values so large that * `toISOString` would throw. */ export declare function slackTsToIsoTimestamp(ts: string | undefined): string | undefined; /** One combined signal for the whole context phase, plus its cleanup. */ export declare function withContextDeadline(signal: AbortSignal, timeoutMs: number): { readonly signal: AbortSignal; readonly dispose: () => void; }; /** Resolved when the phase deadline wins the race against in-flight work. */ export declare const SLACK_CONTEXT_DEADLINE_EXCEEDED: unique symbol; /** * `work`, unless `signal` aborts first — in which case the deadline sentinel is * returned and the in-flight work is abandoned. * * Aborting the signal is not enough on its own: a `SlackWebApi` implementation is * free to ignore `options.signal`, and then awaiting it would let the context * phase outlive its budget and delay the turn. Racing makes the bound hold no * matter how the client behaves. * * Rejections still propagate, so the caller can classify a failure (and latch a * rate-limit cooldown) rather than mistaking it for a timeout. */ export declare function raceAgainstDeadline(work: Promise, signal: AbortSignal): Promise; //# sourceMappingURL=thread-context.d.ts.map