/** * One responder per owner message in Slack rooms — the pure parts * (specs/2026-10-03-one-responder-per-message.md). * * - {@link threadOwnerFromSlack}: who owns a Slack thread, read from the * thread itself, so every machine names the same agent. * - {@link findPriorRoomAnswer} / {@link heldReplyMessage}: the reply gate — * a colleague already answered the owner's latest message in this thread. * * Incident (2026-10-03, #content-team): the owner's un-@'d reply to Atlas's * D-92 reminder resolved the card (delivered to Atlas) and was also * broadcast to the room; Atlas answered, and Ella answered the same thing * six minutes later. * * @module services/slack/room-responder */ import type { SlackContextMessage, SlackThreadContext } from '../../types/slack.types.js'; import type { ChatMessageDTO } from '../chat-v2/types.js'; /** An agent of the room, on this machine or another. */ export interface RoomAgentRef { /** Session name (as this machine knows it; Cloud's for a remote agent) */ session: string; /** Display name */ name: string; /** Whether it runs on this machine */ local: boolean; } /** What {@link buildRoomAgentDirectory} needs. */ export interface RoomAgentDirectoryInput { /** This machine's room members, with their own bot user id when installed */ local: ReadonlyArray<{ session: string; name: string; botUserId?: string | null; }>; /** Room members on other machines (Cloud's room roster minus this machine) */ remote: ReadonlyArray<{ session: string; name: string; }>; } /** Maps Slack authors and names to room agents. */ export interface RoomAgentDirectory { /** The agent that wrote a Slack message, or null (a person, an unknown bot, or an unclear name) */ authorOf(m: SlackContextMessage): RoomAgentRef | null; } /** * Whether two display names name the same agent: equal (case-insensitive), * or one is the other plus a team suffix — "Ella" / "Ella (Crewly Marketing)". * * @param a - A name * @param b - Another name * @returns True when they match */ export declare function sameAgentName(a: string, b: string): boolean; /** * Build the directory every machine uses to read the same thread the same way. * * A post by a local agent's own bot is that agent. Any other bot post is * matched by name against the remote members — and against local members * when it was posted with a username override (an agent with no app of its * own). A name that matches several agents is unclear and maps to nobody. * * @param input - Local and remote room members * @returns The directory */ export declare function buildRoomAgentDirectory(input: RoomAgentDirectoryInput): RoomAgentDirectory; /** * Whether a Slack post is a decision card or a card reminder — the post an * owner's reply in that thread answers. Cards carry `Decision D-n` (or * `[D-n]` when titled by a ticket); the reminder says "Still waiting on you". * * @param text - Normalised Slack text * @returns True for a card or reminder */ export declare function isDecisionCardText(text: string): boolean; /** Who owns a Slack thread, and by which rule. */ export interface SlackThreadOwner { agent: RoomAgentRef; /** * `card`: a decision card / reminder went up since a person last spoke — * the reply answers it, and the card's machine handles it whatever Cloud's * presence says. `last-speaker`: the agent that spoke last. */ via: 'card' | 'last-speaker'; } /** * Who owns a Slack thread (specs/2026-10-03-one-responder-per-message.md §1 c): * * 1. a decision card or reminder an agent posted since a person last spoke * in the thread — the reply answers that card (the decision path's own * rule, read from Slack so every machine agrees); * 2. otherwise the agent that spoke last (owner's rule, 2026-09-21: a bare * follow-up addresses whoever just spoke). * * @param ctx - The Slack thread before the owner's message (oldest first) * @param dir - Room directory * @returns The owner and the rule, or null when no agent has spoken in the thread */ export declare function threadOwnerFromSlack(ctx: SlackThreadContext | null | undefined, dir: RoomAgentDirectory): SlackThreadOwner | null; /** An answer already in the thread after the owner's latest message. */ export interface PriorRoomAnswer { /** Who wrote it (display name when known, else its session) */ by: string; /** The agent's session (local) or the recorded sender id (a colleague's Slack post) */ bySession: string; /** The start of the answer */ excerpt: string; /** Its chat message id */ messageId: string; /** False when its writer was not a chosen responder (another agent already answered, crewly#1376) */ byResponder?: boolean; /** The posting agent's own answer, written with app-comments and shown in this thread */ own?: boolean; } /** * The posting agent's own app-comments reply to the owner's latest message in * this thread. Such a reply is written into the room thread (and its Slack * thread) for it, so a `reply` after it repeats the answer: Lyra's #16 answer * showed up twice plus a resolve line on 2026-10-19. * * @param thread - The thread's rows, oldest first * @param session - The agent about to post * @param name - Its display name, when known * @returns The answer already there, or null */ export declare function ownAppCommentAnswer(thread: readonly ChatMessageDTO[], session: string, name?: string): PriorRoomAnswer | null; /** * The responders the harness chose for a room message, as recorded on its row. * * @param m - Chat row * @returns Sessions (local or Cloud names), empty when none was recorded */ export declare function chosenRespondersOf(m: Pick): string[]; /** * When the owner last wrote in a room thread. * * @param thread - The thread's rows, oldest first * @returns Epoch ms of the owner's newest row, or null when the owner never wrote in it */ export declare function lastOwnerMessageAt(thread: readonly ChatMessageDTO[]): number | null; /** * The reply gate (specs/2026-10-03-one-responder-per-message.md §2). A post * is held only when ALL of these hold: * * - the owner's latest message in the thread has recorded responders, and * the agent posting is not one of them nor @'d in it; * - the agent owes no earlier owner message in this thread (it was the * responder for, or @'d in, one it has not posted after); * - an agent already answered (an interim note is not an answer) — one of * those responders, else any other agent of the room (crewly#1376: three * agents answered one 「你再发一次」) — and that answer does not @ the * agent posting. * * @param thread - The thread's rows, oldest first * @param session - The agent about to post * @param nameFor - Display name of a session, when known * @returns The responder's answer when the post should be held, else null */ export declare function findPriorRoomAnswer(thread: readonly ChatMessageDTO[], session: string, nameFor?: (session: string) => string | undefined): PriorRoomAnswer | null; /** * What the agent is told when its post is held. Never "drop it": an agent * that was asked must still be able to answer. * * @param prior - The answer already there * @returns English harness text */ export declare function heldReplyMessage(prior: Pick): string; //# sourceMappingURL=room-responder.d.ts.map