/** * Phase 0 keystone — `botmux dispatch` pure core. * * The orchestrator (主 bot) splits a big project into sub-projects and assigns * each to a small group of bots (often a coder + a reviewer). To open a * sub-project it seeds a fresh Lark thread and @-mentions the assigned bots so * each spawns its own thread-scoped session (botmux's existing one-thread-one- * session routing; bot→bot @ inside a thread is ungated — see * event-dispatcher.ts decideRouting + the chat-scope-only foreign-bot gate). * * This module is the pure, I/O-free core: parse the `--bot` specs and build the * two messages (a top-level seed = the thread root, and the threaded kickoff * that @-mentions the bots with their roles + the brief). The CLI shell * (cli.ts) performs the actual sendMessage + replyMessage. */ import { resolveSendTarget, type SessionReplyTarget } from './reply-target.js'; export { resolveSendTarget }; export interface DispatchBot { /** open_id as seen by the orchestrator's app (from ). */ openId: string; /** Display name, for readable @ rendering / division-of-labor lines. */ name?: string; /** Short role label, e.g. "coder" / "reviewer". */ role?: string; } export type PostNode = { tag: 'text'; text: string; } | { tag: 'at'; user_id: string; }; export type PostParagraph = PostNode[]; export interface DispatchMessages { /** Plain-text seed (the thread root) — the human-visible "this sub-project exists" header. */ seedText: string; /** Lark 'post' content (paragraphs of nodes) for the threaded kickoff. */ threadContent: PostParagraph[]; /** open_ids @-mentioned in the kickoff — the bots that will be triggered. */ mentionedOpenIds: string[]; } /** * Compatibility protocol for legacy/cross-machine `--bot` dispatches. * * Keep the marker after the positional report text. Older receivers do not know * this boolean flag, but their generic positional parser safely ignores an * unknown trailing flag instead of consuming the report text as its value. */ export declare function appendLegacyDispatchReportProtocol(brief: string): string; /** Bind a stable local dispatch to its exact report destination. */ export declare function appendDispatchReportProtocol(brief: string, dispatchRootId: string): string; /** Additionally ask the assignee to leave a human-visible copy in the task topic. */ export declare function appendDispatchCompletionProtocol(brief: string): string; export declare function buildDispatchCompletionBrief(input: { brief: string; dispatchRootId: string; exactReportRootEnabled: boolean; sameTopicSendEnabled: boolean; }): string; /** * Parse a `--bot` spec `openId[:name[:role]]` into a {@link DispatchBot}. * Mirrors the `--mention "open_id:Display Name"` convention, with an optional * trailing role segment. */ export declare function parseDispatchBotSpec(raw: string): DispatchBot; /** * Build the seed + threaded-kickoff messages for one sub-project dispatch. * Throws when there is no title or no bot to dispatch to. */ export declare function buildDispatchMessages(input: { title: string; brief: string; bots: DispatchBot[]; }): DispatchMessages; /** * Build the "repo prime" message: a `/repo ` command @-mentioning the * target bots, sent as a **plain text message** — exactly like a human typing * "@bot /repo ". Sent as the first message into a freshly-seeded thread, * it makes each sub-bot's daemon resolve the working dir and spawn its CLI * **idle** (no repo-selection card, no manual "直接开始" click) — i.e. standby. * * Why text (not a structured `post`): the receiving daemon parses a text * message's @ via `resolveMentions` (the same clean path a human @ goes * through), whereas a `post`'s at/text nodes go through `renderPostNode`, which * drops the `/repo` argument in the live event — see the dispatch debugging * notes. `/repo` is an existing botmux command, so this needs no receiving-side * change. The `` tags come first so that, once the receiving daemon strips * leading mentions, it sees `/repo ` as the command. */ export declare function buildRepoPrimeText(input: { path: string; bots: DispatchBot[]; }): { text: string; mentionedOpenIds: string[]; }; /** * Build the report-back message a dispatched sub-bot sends to its orchestrator. * * In 多话题协作模式 a sub-bot must NOT @ the orchestrator in its own sub-topic — * that thread has no orchestrator session, so the orchestrator's daemon would * spawn a fresh, context-less one. Instead `botmux report` sends this content * **into the orchestrator's own thread** (recorded by `botmux dispatch`), * @-mentioning the orchestrator so its existing, context-rich session is the one * that wakes up. This is the pure content builder; cli.ts resolves the coords * and performs the reply. * * The @ stays on the first line so the mention renders next to the headline; * any further lines become their own paragraphs (Lark 'post' shape). */ export declare function buildReportContent(input: { orchOpenId: string; content: string; }): PostParagraph[]; /** * Footgun guard for the orchestrator→sub-bot direction. A dispatched sub-bot's * session lives **inside its sub-topic**, so @-mentioning it from the main chat * (e.g. `botmux send --mention `) doesn't reach that session — it * spawns a fresh, context-less one in the chat (the mirror of the report-back * problem). To talk to a sub-bot the orchestrator must send INTO its sub-topic * (`botmux dispatch --into --bot `). * * Given the dispatch registry (seed → {orchChatId, bots}) and the set of seeds * whose sub-topic is still active, return the sub-topic seed to redirect to when * `mentionOpenId` is a sub-bot dispatched into an active topic of `chatId`; * otherwise null. Only fires for live topics so stale entries don't block sends. */ export declare function findSubBotTopic(input: { mentionOpenId: string; chatId: string; registry: Record; activeSeeds: Set; }): string | null; /** A quote reply references a root but does not enter that root's thread. */ export declare function threadRootForReachability(target: SessionReplyTarget): string | undefined; type ReachabilitySession = { status: 'active' | 'closed'; scope?: 'thread' | 'chat'; chatId: string; rootMessageId: string; larkAppId?: string; deferredScheduleRun?: unknown; vcMeetingReceiver?: unknown; }; /** * Identify active chat sessions whose bot is still configured to fold a * mention back into that shared session. Mode lookup failures fail closed. */ export declare function foldableChatSessionAppIds(input: { sessions: Iterable; targetChatId: string; outboundMode: SessionReplyTarget['mode']; resolveMode: (larkAppId: string, chatId: string) => 'chat' | 'shared' | 'new-topic' | 'chat-topic' | undefined; resolveChatMode: (chatId: string) => Promise<'group' | 'topic' | 'p2p' | 'unknown' | undefined>; }): Promise>; /** * Resolve sender-scoped open_ids for bots that already have an active session * at the current conversation anchor. These peers are reachable here, so an * older dispatch record for the same bot must not be presented as the target. */ export declare function activeConversationBotOpenIds(input: { sessions: Iterable; targetChatId: string; outboundRootMessageId?: string; foldableChatAppIds?: Set; botEntries: Array<{ larkAppId: string; botName: string | null; }>; crossRef: Record; }): Set; /** Resolve the stable Review/orchestrator addressee independently of placement. */ export declare function resolveReportRecipient(input: { creatorOpenId?: string; ownerOpenId?: string; quoteTargetSenderOpenId?: string; }): string | undefined; /** * Compatibility view of registry coordinates plus recipient. * * `cmdReport` no longer treats these coordinates as the ordinary no-registry * placement. It uses them only to preserve a matching dispatch route; otherwise * {@link resolveReportPlacement} inherits the executing conversation turn. */ export declare function resolveReportTarget(input: { registryEntry?: { orchChatId?: string; orchScope?: string; orchRoot?: string; }; sessionChatId?: string; creatorOpenId?: string; ownerOpenId?: string; quoteTargetSenderOpenId?: string; }): { orchChatId?: string; orchScope: string; orchRoot: string; orchOpenId?: string; }; export type ReportPlacementSource = 'explicit-into' | 'explicit-top-level' | 'dispatch-registry' | 'legacy-dispatch-fallback' | 'current-turn' | 'session-default'; /** * Resolve only the visible placement of a `botmux report`. * * The report recipient is intentionally resolved separately by * {@link resolveReportRecipient}: choosing where the message is shown must never * change who is @-mentioned. Explicit placement wins, a dispatch registry keeps * its existing orchestrator-return semantics, and an explicitly marked legacy * cross-machine dispatch without a local registry keeps the old top-level * compatibility fallback. Ordinary reports reuse the same turn-bound placement * rules as `botmux send`. */ export declare function resolveReportPlacement(input: { into?: string; topLevel?: boolean; registryTarget?: SessionReplyTarget; legacyDispatch?: boolean; chatScope: boolean; chatId: string; rootMessageId: string; replyTargetRootId?: string; replyTargetTurnId?: string; replyTargetQuoteOnly?: boolean; currentTurnId?: string; }): { target: SessionReplyTarget; source: ReportPlacementSource; }; export interface DispatchRegistryEntry { orchChatId?: string; orchScope?: string; orchRoot?: string; orchAppId?: string; orchSessionId?: string; createdAt?: string; } /** * Resolve the dispatch record for either a normal thread session or a * regular-group chat-scope session folded from a dispatch topic. * * Folded sessions are keyed by chatId, while the registry is keyed by the seed * message id. Their currentReplyTarget is usable only when it belongs to the * CLI's executing turn. Historical replyThreadAliases deliberately do not * participate: they have no turn id and could route a later ordinary report * back into a stale dispatch. */ export declare function findDispatchRegistryEntry(input: { registry: Record; dispatchRootId?: string; sessionScope?: 'thread' | 'chat'; rootMessageId?: string; currentReplyTargetRootId?: string; currentReplyTargetTurnId?: string; currentTurnId?: string; }): { key: string; entry: DispatchRegistryEntry; } | undefined; export interface DispatchAcceptanceSession { larkAppId?: string; chatId?: string; scope?: 'thread' | 'chat'; pid?: number; workerGeneration?: number; rootMessageId?: string; status?: string; queued?: boolean; createdAt?: string; lastMessageAt?: string; lastCliInput?: string; currentReplyTarget?: { rootMessageId?: string; turnId?: string; updatedAt?: string; }; replyTargets?: Record; replyThreadAliases?: Record; dispatchInputReceipts?: Record; } /** * Persist the worker's exact input-queue commit against the immutable inbound * turn and topic root. Returns false when the current session cannot prove the * turn→root relation; callers must then fail closed and leave no receipt. */ export declare function recordDispatchInputCommit(session: DispatchAcceptanceSession, turnId: string, workerGeneration: number, committedAt?: string): boolean; /** * Return the exact local Bot app identities whose persisted session state proves * that a dispatch message reached the intended chat/topic after it was sent. * * A Lark send acknowledgement only proves transport acceptance. This second * acknowledgement is deliberately based on the receiver daemon's own session * store, and supports both normal thread sessions and regular-group chat-scope * sessions that retain the dispatch root as a reply-thread alias. */ export declare function acceptedDispatchBotAppIds(input: { sessions: Iterable; targetAppIds: string[]; chatId: string; threadRootId: string; turnId: string; notBeforeMs: number; isWorkerAlive: (pid: number) => boolean; clockSkewMs?: number; }): string[]; /** * The footgun check shared by `botmux send`'s explicit-mention guard AND its * prose `@Name` auto-injection: returns the sub-topic seed if `mentionOpenId` is * a dispatched sub-bot in an active topic that is NOT reachable in the current * conversation (so @-ing it here would spawn a context-less session), else null. * * The bot I'm replying to (`quoteTargetSenderOpenId`) and bots in * `reachableOpenIds` are reachable right here, so an unrelated older dispatch * topic is never recommended for them. */ export declare function offTopicSubBotTopic(input: { mentionOpenId: string; quoteTargetSenderOpenId?: string; reachableOpenIds?: Set; chatId: string; registry: Record; activeSeeds: Set; }): string | null; /** * Decide which names of a candidate bot are eligible for prose `@Name` * auto-mention injection in `botmux send`. * * The fan-out bug: a bot writes "@Codex review" in its message; the injector * matches each bot by **botName OR cliId**, and the cliId ("codex") is a shared * *type* alias — so "@Codex" matches every codex-type bot (Codex分身, Codex二号分身, * ttadk(codex), aiden x codex…) and pulls them ALL into the topic, each spawning * a session and replying. * * Fix: the unique `botName` is always eligible (so first-time @-invites still * work), but the type-generic `cliId` alias is eligible **only when this bot is * actually in the current conversation** (`convoBotAppIds` = bots with an active * session in this thread / chat). So "@Codex" resolves to the one codex bot * collaborating here, not every same-type bot. `selfAliases` (the sender's own * name/cliId) are always excluded. */ export declare function eligibleAutoMentionAliases(input: { botName?: string; cliId?: string; larkAppId?: string; selfAliases: Set; convoBotAppIds: Set; }): string[]; //# sourceMappingURL=dispatch.d.ts.map