/** * mailbox-loop — Agent-loop integration for mailbox checking. * * Integrates the inter-agent mailbox into the agent's iteration cycle. * Before each LLM call, checks for unread messages from subagents and other * agents. Normal surfaces inject message content inline. Minimal surfaces may * use background delivery: routine chatter remains telemetry-only, while * actionable asks/results/steers still reach the agent. * * Uses the project-level GlobalMailbox for cross-session communication. * * ## Type-based dispatch * * The dispatch logic in this file is the **receive-side** of the type * semantics contract (the send-side lives in `mailbox-message-codec.ts`). * * Every type must have explicit handling. The contract is enforced by * `MAILBOX_TYPE_PROPERTIES` in mailbox-types.ts: adding a new type to the * `MailboxMessageType` union requires a corresponding entry. * * @module mailbox-loop */ import type { Mailbox, MailboxMessage } from '../coordination/mailbox-types.js'; import type { TextBlock } from '../types/blocks.js'; import type { Message } from '../types/messages.js'; export interface MailboxLoopOptions { /** * The mailbox instance, or a getter that returns the current mailbox. * A getter is used when the agent may switch projects at runtime — * the mailbox resolves from the current `ctx.projectRoot` on each * check rather than being captured at construction time. */ mailbox: Mailbox | (() => Mailbox); /** * The agent's globally unique mailbox identity (e.g. `leader@a1b2c3d4`, * session-bound). Read receipts are recorded under this id, so two * sessions whose leaders share a base name never consume each other's * receipts. Pass a GETTER when the identity can change at runtime (an * in-process session swap moves the leader onto a new session tag). */ agentId: string | (() => string); /** Current roster role, used by leader-only delivery filtering. */ role?: string | (() => string | undefined) | undefined; /** * Additional addresses this agent also answers to — typically the bare * base id (`leader`). Lets other agents (and humans) address "leader" * without knowing the session tag; every live leader session receives it. */ aliases?: string[] | undefined; /** Optional delivery predicate applied before dedup/read receipts. */ include?: ((message: MailboxMessage) => boolean) | undefined; /** * Current session id. When provided, mail addressed to `@session:` is * delivered alongside direct, alias, and project-broadcast mail. */ sessionId?: string | (() => string) | undefined; /** Mark returned messages as read. Defaults to true for normal delivery. */ ack?: boolean | undefined; /** * ISO timestamp below which project-wide broadcasts (`to: '*'`) are ignored. * Defaults to the moment this checker is constructed — i.e. a broadcast is * delivered only to sessions that were live when it was sent. * * Without this floor, every NEW session replayed the whole retained * broadcast backlog. Read receipts are keyed by `agentId`, which is derived * from the session id (`base@sha256(sessionId)`), so a new session gets an * identity that appears in no existing `readBy` map and `unreadBy` matches * everything within the 24h retention window. Observed in the wild: 630 * messages carrying 26,406 ack records — 240 distinct reader identities, * single messages acked up to 61 times — with leaders repeatedly reasoning * about "peer noise" from work that had already been committed. * * Scoped to broadcasts on purpose. Directed mail (unique id, base alias, * `@session:` scope) was addressed deliberately and must still be delivered * even if it was sent before this session existed; a human running * `wstack mailbox send --to leader` and then starting a session expects it * to arrive. Broadcasts are ambient announcements ("X joined the fleet", * "task success") and were 619 of those 630 messages. */ broadcastFloor?: string | undefined; } export declare function createMailboxChecker(opts: MailboxLoopOptions): () => Promise; export declare function buildMailboxBtwAwarenessBlock(messages: MailboxMessage[]): { type: 'text'; text: string; }; export declare function buildMailboxBlock(messages: MailboxMessage[]): { type: 'text'; text: string; }; /** * Remove request-scoped mailbox blocks after the provider has evaluated them. * * Mail is folded into a user message before request construction so normal * context-window accounting and provider role alternation still work. Keeping * the raw block after that request, however, makes every later request pay for * and reconsider the same mail. This helper removes only the exact text blocks * injected by the current run. Any assistant response, tool call/result, todo, * or other durable consequence remains in the conversation. */ export declare function removeInjectedMailboxBlocks(messages: readonly Message[], injectedBlocks: readonly TextBlock[]): { messages: Message[]; changed: boolean; }; /** Result of an inject pass — signals an out-of-band control request. */ interface MailboxInjectResult { /** A fresh `control:interrupt` message asked this agent to stop. */ interrupt: boolean; /** Operator-supplied reason for the interrupt, if any. */ interruptReason?: string | undefined; } export type MailboxDeliveryMode = 'inline' | 'background'; export declare function injectPendingMailboxMessages(checkMailbox: () => Promise, foldFn: (block: { type: 'text'; text: string; }) => void, a: { events: { emit: (type: string, payload: unknown) => void; }; logger: { debug?: (...args: unknown[]) => void; }; }, deliveryMode?: MailboxDeliveryMode): Promise; export {}; //# sourceMappingURL=mailbox-loop.d.ts.map