import type { RuntimeEventBus } from '../runtime/events/index.js'; import type { ChannelRenderPhase, ChannelRenderResult, ChannelSurface } from './types.js'; import type { ChannelPluginRegistry } from './plugin-registry.js'; import type { RouteBindingManager } from './route-manager.js'; import type { ChannelRenderAudience } from './render-audience.js'; export { normalizeChannelRenderEventFromRuntime } from './reply-render.js'; export interface TrackedChannelReply { readonly agentId: string; readonly surfaceKind: ChannelSurface; readonly task: string; readonly agentTask?: string | undefined; readonly workflowChainId?: string | undefined; readonly createdAt: number; readonly sessionId?: string | undefined; readonly routeId?: string | undefined; readonly [key: string]: unknown; } /** * A reply that was produced for a conversation but never reached it. * * Handed to the host so the miss lands in the same delivery ledger automation * deliveries use. Without this, "the agent answered and the answer was lost" * and "no message ever arrived" read identically: zero attempts, no record. */ export interface UndeliveredChannelReply { readonly surfaceKind: ChannelSurface; readonly agentId: string; readonly sessionId?: string | undefined; readonly routeId?: string | undefined; readonly phase: ChannelRenderPhase; readonly body: string; readonly reason: string; } export type UndeliveredChannelReplyReporter = (reply: UndeliveredChannelReply) => void; /** A reply that did reach its conversation, for the same ledger. */ export interface DeliveredChannelReply { readonly surfaceKind: ChannelSurface; readonly agentId: string; readonly sessionId?: string | undefined; readonly routeId?: string | undefined; readonly responseId?: string | undefined; } export type DeliveredChannelReplyReporter = (reply: DeliveredChannelReply) => void; interface ReplyPipelineDeps { readonly channelPlugins: ChannelPluginRegistry; readonly routeBindings: RouteBindingManager; readonly runtimeBus?: RuntimeEventBus | null | undefined; readonly now?: (() => number) | undefined; /** Records a produced-but-undelivered reply in the delivery ledger. */ readonly onUndelivered?: UndeliveredChannelReplyReporter | undefined; /** Records a reply that reached its conversation in the delivery ledger. */ readonly onDelivered?: DeliveredChannelReplyReporter | undefined; } export declare class ChannelReplyPipeline { private readonly channelPlugins; private readonly routeBindings; private readonly now; private readonly buffers; /** * Tail of the in-flight delivery chain for each agent, the serialization * point that makes "read the watermark, publish, mark delivered" atomic. * * Two callers reach this concurrently by design: `handleEnvelope` fires on * every bus event, and the daemon's pending-reply poller calls * `deliverProgress(..., force)` on its own 2s tick. Both used to read the * same unmarked watermark while a publish was still in flight, so each one * selected the same events plus whatever had arrived since, the reader got * a ladder of notifications where each body was a strict SUPERSET of the one * before it. The single-call delta was already correct; the interleaving was * not. */ private readonly deliveryChains; private readonly workflowChains; private readonly unsubscribers; private undeliveredReporter; private deliveredReporter; constructor(deps: ReplyPipelineDeps); /** Install (or replace) the ledger reporters after construction. */ setUndeliveredReporter(reporter: UndeliveredChannelReplyReporter | null): void; setDeliveredReporter(reporter: DeliveredChannelReplyReporter | null): void; attachRuntimeBus(runtimeBus: RuntimeEventBus | null): void; dispose(): void; /** * Run `task` with no other delivery for the same agent in flight. * * Serializing the whole read-decide-publish-mark body, rather than only * reserving the watermark before the await, is deliberate. Reserving the * watermark alone stops the superset ladder, but it leaves the two OTHER * pieces of state this method reads before the await and writes after it * racing: `lastDeliveredText` (the identical-body suppression) and * `lastDeliveredAt` (the pacing interval). Under a reserve-only fix two * callers still both pass the pacing check and both publish, so one message * still arrives as two notifications, disjoint instead of nested, which is * a smaller bug of the same kind. With the section serialized, the second * caller observes the first one's marks and correctly suppresses itself. * * Scope is per agent id, so a slow surface can only delay that agent's own * updates, never another agent's. Rejections are absorbed into the chain * tail (callers still see their own), so one failed send cannot poison the * next one. */ private runExclusive; trackPending(pending: TrackedChannelReply): void; untrack(agentId: string): void; has(agentId: string): boolean; getPending(agentId: string): TrackedChannelReply | null; /** * `audience` classifies `explicitText` only, the buffered events carry their * own. It defaults to `operator` for the same reason the field does * everywhere else: a caller who hands over a status string without saying who * wrote it is handing over `AgentRecord.progress`, which is the orchestrator's * running tool name and a scrap of its arguments. That reached the owner's * phone as `registry, email send`. See agents/progress-audience.ts. */ deliverProgress(agentId: string, explicitText?: string, force?: boolean, audience?: ChannelRenderAudience): Promise; private deliverProgressExclusive; deliverFinal(agentId: string, explicitText: string, options?: { readonly keepTracking?: boolean; }): Promise; private deliverFinalExclusive; /** * Report a reply that was produced but never reached its conversation. * * Routed through the SAME delivery ledger the automation deliveries use, so * a "should have sent, did not" is a visible failed attempt rather than an * absence indistinguishable from "nothing happened". */ private reportUndelivered; private handleEnvelope; private handleWorkflowEnvelope; private trackChildPendingReply; private findPendingForWorkflowTask; private associateWorkflowChain; private resolvePolicy; private dispatch; private disposeSubscriptions; } //# sourceMappingURL=reply-pipeline.d.ts.map