import type { DaemonSession } from './types.js'; import type { FrozenSessionReplyContext, FrozenSessionReplyTarget, LarkMention, ReplyTargetEntry, Session, TurnParticipant } from '../types.js'; /** Merge participants by open_id, keeping the richest label (a later entry can * fill a missing name / promote isBot). Order-stable on first appearance so a * candidate list reads in arrival order. */ export declare function dedupeParticipants(list: TurnParticipant[]): TurnParticipant[]; /** Pure core of a message's turn-window contribution (daemon wraps it with live * deps). Its sender plus everyone it @-mentioned, excluding the answering bot * (by `selfOpenId` OR `selfAppId` — a self @ often arrives in app_id form, so * open_id alone would miss it and wrongly mark a plain 1v1 incomplete). * `participants` holds ONLY executable receiver-scoped open_id candidates (so * `botmux send` can hand them back as `--mention `), each labelled bot * (`isMentionBot` proves a known peer, or sender is a platform-stamped bot) / * unknown (NOT provably human — `isMentionBot=false` does not prove a person; a * third-party bot isn't in the cross-ref). Returns `incomplete: true` when a * real, NON-self @-mention could not be reduced to a usable open_id (app_id / * user_id / union_id form — parser leaves `openId` undefined): that counterpart * is NOT listed as a candidate but forces the window ambiguous so the gate * demands an explicit decision rather than risk a wrong single-target @. Sender * name is best-effort — omitted when unknown. */ export declare function buildTurnParticipantsFrom(sender: { openId?: string; isBot?: boolean; name?: string; }, mentions: LarkMention[] | undefined, selfOpenId: string | undefined, isMentionBot: (openId: string) => boolean, selfAppId?: string): { participants: TurnParticipant[]; incomplete: boolean; }; export type SessionReplyTarget = { mode: 'plain'; chatId: string; } | { mode: 'thread'; rootMessageId: string; } | { mode: 'quote'; rootMessageId: string; }; /** Stable key for one visible Lark destination. Keep the reply mode in the key: * a quoted top-level reply and a reply inside that message's thread share the * same message id but are different visible surfaces. */ export declare function replyTargetKey(target: FrozenSessionReplyTarget): string; /** Freeze the visible Lark destination for one inbound turn before any * lifecycle mutation can replace or remove its session. `replyRootId` is * supplied only for a real chat-scope thread fold-back; quote-only turns keep * the same root but use ordinary reply semantics instead of reply_in_thread. */ export declare function resolveInboundReplyTarget(args: { scope: 'chat' | 'thread'; chatId: string; threadRootId: string; replyRootId?: string; quoteOnly?: boolean; }): SessionReplyTarget; /** Bound on `Session.replyTargets`: long-lived sessions could otherwise grow * without limit. An evicted turn may use a legacy slot only when its turnId * still matches exactly; it never borrows a later turn's sender. */ export declare const REPLY_TARGETS_MAX = 32; /** Prune `targets` down to REPLY_TARGETS_MAX oldest-first, IN PLACE, and return * the new prune high-water mark = max(existing watermark, latest `updatedAt` * among the entries actually evicted). Both replyTargets writers * (beginReplyTargetTurn + trigger-final-suppression's inheritTriggerReplyAnchor) * MUST route eviction through here so a pruned sibling can never silently * under-count a turn's participant window — `botmux send` compares the returned * watermark against the turn window to decide incompleteness. */ export declare function pruneReplyTargets(targets: Record, prevPrunedThrough: string | undefined): string | undefined; export interface TurnReplyTarget extends Omit { turnId: string; updatedAt?: string; } /** Reply context for one exact turn. The per-turn entry is authoritative. Old * persisted sessions may fall back to the single slots only when those slots * explicitly identify the requested turn. */ export declare function pickTurnReplyTarget(s: Pick, currentTurnId: string | undefined): TurnReplyTarget | undefined; /** Whether `turnId` is a chat-scope substitute turn that disables the * streaming card. Thread-scope substitute turns keep their normal card. With * no turn context, falls back to the latest-accepted chat turn's flag; callers * with a turnId get an exact per-turn answer so queued normal/substitute turns * cannot inherit each other's card state. */ export declare function isSubstituteTurn(ds: Pick, turnId?: string): boolean; export declare function resolveSessionReplyTarget(ds: Pick, turnId?: string): SessionReplyTarget; export declare function resolveSendTarget(opts: { into?: string; topLevel: boolean; chatScope: boolean; chatId: string; rootMessageId: string; replyTargetRootId?: string; replyTargetTurnId?: string; replyTargetQuoteOnly?: boolean; currentTurnId?: string; }): SessionReplyTarget; export declare function beginReplyTargetTurn(ds: DaemonSession, replyRootId: string | undefined, turnId: string, nowIso?: string, opts?: { quoteOnly?: boolean; substitute?: boolean; senderOpenId?: string; participants?: TurnParticipant[]; participantsIncomplete?: boolean; }): void; /** Resolve a turn's immutable inbound destination, falling back only for * legacy/non-Lark turns that predate the bounded registry. */ export declare function frozenReplyContextForTurn(ds: Pick, turnId?: string): FrozenSessionReplyContext; /** Window within which sibling turn records are treated as the SAME turn for * --mention-back ambiguity. Type-ahead follow-ups each land as their own * per-turn record (distinct message_id), and the model may resolve * BOTMUX_TURN_ID to whichever was processed last — so `botmux send` unions the * participants of every record updated within this window of the resolved * turn. Deliberate conservative approximation: uncertainty (an incomplete * window, a pruned sibling, no window at all) fails toward explicit addressing, * and erring wide can only over-suggest an explicit --mention, never wrongly * auto-@ the wrong single counterpart. 90s comfortably spans a busy CLI batch * while not bleeding into an unrelated later conversation. */ export declare const TURN_WINDOW_MS = 90000; export interface TurnWindow { /** Executable receiver-scoped open_id candidates in the window (deduped). */ participants: TurnParticipant[]; /** True when the set may be UNDER-counted — a non-open_id @ we couldn't * resolve, a window-relevant sibling pruned by the bounded map, or the turn * window itself is unknown. Callers must fail toward an explicit --mention. */ incomplete: boolean; } /** The turn-window counterpart set for `currentTurnId`: the resolved turn's own * participants unioned with those of any sibling turn record updated within * TURN_WINDOW_MS (covers type-ahead follow-ups that folded into this model * turn under different message_ids). `incomplete` is set when the set can't be * proven complete — no anchor, a window record self-marked incomplete * (unresolved @), or the prune watermark reaches into the window (a pruned * sibling may have carried an unseen counterpart). */ export declare function collectTurnWindowParticipants(s: Pick, currentTurnId: string | undefined): TurnWindow; /** * Effective turnId for a daemon-side message. Callers that know their turn * (worker final_output, placeholder cards) pass it explicitly and the * stale-turn gate in resolveSessionReplyTarget stays authoritative. Callers * with NO turn context of their own (the worker's first streaming card, * crash notices) fall back to the session's current reply-target turn — in a * shared fold-back topic they then follow the conversation into the thread * instead of leaking to the chat top level. */ export declare function fallbackTurnId(ds: Pick, turnId: string | undefined): string | undefined; export declare function syncReplyTargetState(ds: DaemonSession, s?: Session): void; /** * Rebind reply metadata after a live DaemonSession is moved to another Lark * destination or assigned a replacement Session record. Source-chat message * ids must never survive as routing authority in the new destination. * * Per-turn sender/participant attribution is retained for buffered inputs, but * every visible target is rewritten to the session's new canonical surface. * Callers detach/clear the old live card before invoking this helper, so its * persisted destination key is cleared as part of the same lifecycle boundary. */ export declare function rehomeReplyTargetState(ds: DaemonSession): void; //# sourceMappingURL=reply-target.d.ts.map