/** * IrcBus - Process-global mailbox bus for agent-to-agent messaging. * * Replaces the old auto-reply model: a `send` never blocks on the recipient * generating anything. Delivery resolves the recipient via the global * AgentRegistry — parked and killed agents are revived through the * AgentLifecycleManager, idle agents are woken with a real turn, and busy * agents receive the message as a non-interrupting aside at the next step * boundary (see AgentSession.deliverIrcMessage). */ import { type IrcDeliveryReceipt, type IrcMessage } from "@oh-my-pi/pi-tui/tools/irc"; import { type AgentLifecycleApi } from "../registry/agent-lifecycle.js"; import { type AgentRegistryApi } from "../registry/agent-registry.js"; /** Which messages a bus {@link IrcBusApi.wait} consumes. */ export interface IrcWaitFilter { /** Only messages from this agent. */ from?: string; /** * Skip broadcast legs and progress notes, which ask the waiter nothing: they * reach it through ordinary delivery, a non-interrupting aside when it is busy. */ directOnly?: boolean; /** * With `directOnly`, still take a broadcast from these agents: the waiter * asked them something, and a broadcast answers it (see {@link IrcBusApi.unansweredSends}). */ repliesFrom?: readonly string[]; } /** Whether a bus wait under `filter` consumes `msg`. */ export declare function ircWaitAccepts(filter: IrcWaitFilter, msg: IrcMessage): boolean; /** Public surface of the process-global IRC bus, implemented locally on main and by a proxy in workers. */ export interface IrcBusApi { /** Fire-and-forget delivery; the receipt reports how the message reached the recipient. */ send(msg: Omit): Promise; /** Whether `from` successfully sent `to` anything, progress included, at or after `sinceTs`. */ sentSince(from: string, to: string, sinceTs: number): boolean; /** Agents `from` messaged directly at or after `sinceTs` that have not answered since. */ unansweredSends(from: string, sinceTs: number): Array<{ to: string; sentAt: number; promised: boolean; }>; /** Block until a message for `agentId` that `filter` accepts arrives; null on timeout. */ wait(agentId: string, filter: IrcWaitFilter, timeoutMs: number, signal?: AbortSignal, options?: { drainPending?: boolean; }): Promise; /** Consume the oldest pending message for `agentId` (optionally from `from`). */ take(agentId: string, from?: string): IrcMessage | undefined; /** Unread count for the local Agent Hub overlay. */ unreadCount(agentId: string): number; /** Count a settled job of agent `from` owned by `to` as `from`'s reply to `to`. */ recordReply(from: string, to: string): void; } export declare class IrcBus implements IrcBusApi { #private; /** The installed bus; lazily a local {@link IrcBus} unless a worker installed a proxy. */ static global(): IrcBusApi; /** Install the process-global bus. Worker bootstrap only. */ static installGlobal(api: IrcBusApi): void; /** Reset the global bus. Test-only. */ static resetGlobalForTests(): void; /** * Count a settled job of agent `from` owned by `to` as `from`'s reply to * `to`: the job delivers that agent's outcome, so it answers what `to` * asked it before now (see {@link unansweredSends}). With no bus yet no * message was ever sent, so there is nothing to answer. */ static recordReply(from: string, to: string): void; constructor(registry?: AgentRegistryApi, lifecycle?: AgentLifecycleApi); /** * Fire-and-forget delivery. Never blocks on the recipient generating * anything: the receipt reports how the message reached the recipient * (waiter/aside = "injected", idle wake = "woken", park revival = * "revived"), not what they did with it. * * Mailbox semantics: a successfully delivered message never lingers in * the recipient's mailbox — injection/wake puts the full body into their * context, so buffering it too would double-deliver via a later * `wait`/`inbox` and inflate unread counts. Only a failed live hand-off * is buffered for the recipient to drain later. */ send(msg: Omit): Promise; /** Instance half of {@link IrcBus.recordReply}: records the job-carried reply on this bus. */ recordReply(from: string, to: string): void; /** * Whether `from` successfully sent `to` anything, progress included, at or * after `sinceTs`. The wake-turn relay uses it to skip agents that already * answered their waker themselves, or promised it a later answer. */ sentSince(from: string, to: string, sinceTs: number): boolean; /** * Agents `from` messaged directly at or after `sinceTs` that have not * answered since, with when the latest of those messages went out: the * replies a `wait` can block for. A message back answers, and so does a * settled job of the agent `from` owns, which delivers its result. Progress * answers nothing; it marks the request `promised`, and from then on only * an explicit message answers it (a job settling the turn that sent the * progress must not). "Since" is by send order, not timestamp, so an answer * to an earlier question never settles a later one sent within the same * millisecond. */ unansweredSends(from: string, sinceTs: number): Array<{ to: string; sentAt: number; promised: boolean; }>; /** * Block until a message for `agentId` that `filter` accepts arrives; * consume + return it. Null on timeout (`timeoutMs <= 0` waits * forever). Rejects when `signal` aborts. By default, already-buffered * mail satisfies the wait before parking a future waiter; callers that * need a strictly future reply can disable that drain. */ wait(agentId: string, filter: IrcWaitFilter, timeoutMs: number, signal?: AbortSignal, options?: { drainPending?: boolean; }): Promise; /** * Consume the OLDEST pending message for `agentId` (optionally restricted * to `from`), leaving the rest of the mailbox intact. This is the exact * atomic step `wait` performs on entry, exposed for callers that must not * block without draining the entire backlog. */ take(agentId: string, from?: string): IrcMessage | undefined; /** Unread count for the local Agent Hub overlay. */ unreadCount(agentId: string): number; }