/** * The lifecycle seam between a connector's hooks and the AG-UI emitter. * * **This file exists so the cutover is a SWAP, not a swap plus a new lifecycle.** `TranscriptMirror` * presents a synchronous `adopt(path)` / `flush(path)` pair to the hook relay, because that is what * a hook can call: the relay must reply immediately and cannot await a publish. {@link AguiEmitter} * is the opposite shape: `start()` is async (it resolves the channel, runs the replica preflight, * and settles any frame the WAL left pending) and `pump()` is async. Something has to hold the * async thing behind the sync surface, and if that something is written in the same commit that * deletes the mirror, then the irreversible step also carries an untested lifecycle. * * So it lands first, against the shape the mirror already proved, and the cutover becomes a * substitution of one object for another behind an unchanged call pattern. * * **WHY LAZY.** The emitter cannot be built at construction time. Its {@link DurableSource} is the * session's transcript, whose path the connector does not know until a hook hands it over, and * `start()` reaches the broker — work that must not run for a session that never emits. First * `adopt` is the earliest moment the emitter is both constructible and needed. */ import type { CotalMeta } from "./agui.js"; import type { AguiEmitter } from "./agui-emitter.js"; /** * The connector's hooks into an {@link AguiEmitterHolder}. * * Named rather than positional because the types cannot tell them apart: a `runMeta` provider is * assignable to `onRunClosed`, so a position would let it land there, typecheck, and lose its * metadata with nothing reporting why. */ export interface AguiEmitterHolderHooks { /** Where a failure goes. Required, and not defaulted to a swallow: the holder runs behind a hook * that must not throw, so the only way a failure reaches a human is if the caller is made to say * where it goes. */ onError: (e: Error) => void; /** * Told which run {@link AguiEmitterHolder.closeRun} closed, so the connector's own mapper can * forget the run it will no longer attribute records to. Optional, and it is NOT the holder * doing the forgetting: which state a record mapper keeps is the connector's business, and a * holder that reached into it would be a second place that decides what a run is. Without it a * mapper that still believes the run is open would emit under a `runId` the published stream has * already closed, and the emitter would refuse the batch. */ onRunClosed?: (runId: string) => void; /** * Rides out a transient endpoint rebuild window before a step pumps or closes a run. Called once * per queued step that needs the broker (a flush, or a close with something open); it resolves * when the endpoint's event plane is live, or rejects to make the whole step fail terminally * exactly as any other emitter failure does. This is the seam the connectors' * `MeshAgent`-adjacent wiring owns: the holder knows WHEN to publish, the caller knows HOW to * observe liveness. Without it, a rebuild window between two pumps reads `max_payload` off a * connection that is not there and kills the seat (#1868). */ waitLive?: () => Promise; /** * The Cotal metadata for the run {@link AguiEmitterHolder.closeRun} is about to close, such as * its stop reason or usage, put on whichever terminal closes it. Asked inside the queued close * rather than taken at call time, because the hook calls `closeRun` before the flush queued * ahead of it has mapped the turn's last records, and those are what the metadata comes from. * Keyed on the run id, so a provider that only knows a different run answers `undefined` * instead of attributing its numbers to this one. */ runMeta?: (runId: string) => CotalMeta | undefined; } /** * The one answer every connector gives when its event plane stops for good, used as a holder's * `onError`. * * Whether a seat may outlive its event plane is the space's policy, so it is decided here and not * by each harness: on a space that requires events the seat stops, because a seat that kept taking * turns would be on the roster while breaking the policy that admitted it. Any other space keeps * the seat running with the reason in its log. */ export declare function eventPlaneStopped(opts: { required: boolean; log: (line: string) => void; stopSeat: () => void; }): (e: Error) => void; /** * Holds at most one {@link AguiEmitter}, started on first adopt. * * `T` is the connector's source record type; the holder never inspects one. It owns four things — * WHEN the emitter starts, that it starts AT MOST ONCE, that everything touching it is serialized, * and carrying the first adopt's opaque context to the connector-owned factory — and deliberately * owns no policy about what that context, an event, or a frame means. */ export declare class AguiEmitterHolder { private readonly startEmitter; private readonly hooks; private emitter?; /** The path this holder is BOUND to. Set before `starting`, so a second path is refused even * while the first start is still in flight. */ private boundPath?; /** * Terminal. Once set, this holder never starts, pumps, or reports success again. * * It does not retry, and that is a decision rather than an omission. A retry on the next hook * would re-run that preflight and a WAL recovery against a stream this holder has already * failed to establish itself on, on a timer set by how often the user happens to type. The * emitter's own answer to an uncertain publish is to halt rather than to limp, and a holder that * quietly reconnected underneath it would reintroduce, one layer up, exactly the silence the * emitter refuses. */ private dead?; /** * Terminal, and NOT a failure. Set by {@link close} when the owner releases this holder: it never * starts, pumps or closes a run again, `failure` stays empty, and `onError` is not called. Kept * apart from `dead` for exactly that reason: a release read as an error puts a line in the log * that tells an operator the event plane broke when it was retired on purpose. */ private shut; /** ALL mutation runs on this chain. Hook events arrive concurrently on the control socket, so * without it two flushes could read the source at the same cursor. */ private chain; /** One shared wait for the endpoint's next live signal, while a queued step is holding for it. * A single listener serves every waiter, and the field clears when it fires. */ private liveWait?; /** * @param startEmitter Builds and starts the emitter for an adopted path. `context` is the opaque * value supplied with the FIRST adopt (if any); the holder only carries it, while the connector * decides what it means. Injected rather than assembled here: the WAL location, the source, and * the record mapper are all connector decisions, and a holder that made them would be a second * place they are decided. */ constructor(startEmitter: (path: string, context: StartContext | undefined) => Promise>, hooks: AguiEmitterHolderHooks); /** True once an emitter is running here. False while a start is still in flight — it reports what * IS, never what is about to be. A dead holder is not running: after {@link die} what IS, is * that this holder is finished, whatever its unstopped emitter still says. */ get running(): boolean; /** The failure that killed this holder, if one did. */ get failure(): Error | undefined; /** True once {@link close} has run. Distinct from {@link failure}: released, not broken. */ get closed(): boolean; /** * Release this holder: refuse every later adopt, flush and run-close, then await what is already * queued. * * THE REFUSAL IS TAKEN SYNCHRONOUSLY, before anything is awaited, so a hook arriving while the * settle is still outstanding is refused rather than racing it. Nothing is cancelled: work already * on the chain runs to completion, so a caller that cannot wait unboundedly has to bound this * itself, exactly as it bounds {@link settled}. * * WHAT IT DOES NOT RELEASE, because those lifetimes are not this object's to decide. The * write-ahead log and the principal lock are created by the `startEmitter` its owner injected: the * log outlives this holder whenever a later start could still recover from it, and the lock * outlives it because a lock is per principal while a holder is per thread. A holder that removed * either would be deciding a policy it cannot see. It closes the seam; the owner closes what the * seam was holding open. * * AND IT REFUSES AT THE DOOR RATHER THAN AT THE WORK, which is the distinction that keeps it from * quietly cancelling. The refusal is in {@link enqueue}, so a call arriving after close never gets * onto the chain, while a flush already on it still reads its source and publishes. A refusal * placed on the far side would have turned every abandoned drain into a silently dropped one, and * an abandoned drain is uncancelled by design: its owner stopped waiting, it did not stop. */ close(): Promise; /** The path this holder bound to on first adopt, if it has adopted. */ get path(): string | undefined; /** * Adopt a transcript path, starting the emitter if this is the first one. * * Synchronous and non-throwing by contract, because a hook calls it. The work lands on the chain. */ adopt(path: unknown, context?: StartContext): void; /** * Adopt if necessary, then drain the source into frames. * * Same contract as {@link adopt}: synchronous, non-throwing, work on the chain. */ flush(path: unknown): void; /** * Close the open run at a turn boundary the record stream cannot see. * * Same contract as {@link adopt} and {@link flush}: synchronous, non-throwing, work on the chain, * because a lifecycle hook calls it and a hook must not be made to wait or to fail. * * It deliberately does NOT start an emitter. A session that never adopted a transcript has nothing * open and nothing to close, and starting one here would reach the broker on the way OUT of a * turn that published nothing. * * **`error` CLOSES THE RUN WITH `RUN_ERROR` INSTEAD**, for a turn the connector knows FAILED. It * is one parameter on the one close rather than a second method, because `RUN_ERROR` closes a run * by itself and a run must never carry both terminals: one call builds one terminal, and the * caller cannot ask for the other afterwards because the run is no longer open. This holder does * not decide what counts as a failure — that is a claim about a harness, and it belongs at the * connector's own mapping site where the harness's record is in hand. */ closeRun(timestamp: number, error?: { message: string; code?: string; }): void; /** * Start at most once, bind the path once. * * Returns `undefined` when there is nothing to run against — a dead holder or a path this holder * cannot take — rather than throwing, so a caller cannot mistake "no emitter" for "pumped". */ private ensureStarted; private die; /** Wait for the endpoint to be live, if the caller supplied a wait. SHARED: while one step * waits, every later step joins the same promise, so a burst of flushes during one outage * produces one listener and one release, and the steps still run one at a time on the chain * (each step's `await` happens inside its own serialized slot, so this sharing cannot * reorder anything). A wait that rejects fails the step, which is the existing terminal path: * liveness-waiting must never swallow an error the emitter would have surfaced. */ private holdForLive; private enqueue; /** Await the queued work. For callers that need a settled point — a shutdown, or a cell. */ settled(): Promise; } //# sourceMappingURL=agui-holder.d.ts.map