/** * Protocol-v2 persistence + fan-out policy for one agent session — the * RunManager feeds every `UiEvent` a runner emits (`SessionOptions.onUiEvent`) * through one sink instance and the sink decides, per the spec's performance * guardrails (§"Normalized agent-event protocol v2" and * `.ai/analysis/cockpit-ui-redesign/protocol-rationale-and-performance.md` §5): * * - `item.delta` is COALESCED per item (~40 ms flush) onto the live wire * only — raw deltas NEVER hit the NDJSON file; replay needs no delta * reduction because every item's final state rides `item.completed`. * - Everything else persists as snapshots via `persist()` (the store stamps * `seq`/`ts`, and `appendEvent` already fans persisted lines out live, so * a persisted event is never double-emitted here). * - `item.updated` hits disk only on a meaningful change (status flip, new * diffs/title/…) — streamed-content-only growth goes to the live wire * ephemerally, mirroring what the deltas already carry. * - `usage.updated` is cumulative per session: persisted only when the * total (or cost) actually moved, mirroring v1's token-usage cadence. * * Output errors are swallowed: v2 is additive and best-effort — a persist * failure (e.g. the run was deleted mid-flush) must never disturb the v1 * stream or crash a timer callback. */ import type { StopReason, UiEvent } from '../core/ui-events.ts'; /** Spec guardrail: coalesce `item.delta` at ~30–50 ms boundaries. */ export declare const DELTA_FLUSH_MS = 40; /** * v2 lines share the NDJSON file and the store's event bus with v1, but ride * a DIFFERENT SSE event name (`ui-event` vs `run-event`) so the legacy UI — * which knew only `run-event` and JSON-dumped unknown types into the * transcript; retired in R7, but the name split stays as wire shape — never * saw them. The discriminator is the dotted namespace: * every v2 type on the wire is dotted (`item.started`, `turn.completed`, …) * and no v1 type is. v2's only undotted type, `image`, never reaches the * store: the sink drops it in favor of the v1 image pipeline (which persists * the file and re-emits a URL instead of raw base64). */ export declare function isV2WireEventType(type: string): boolean; /** A loose event as the store consumes it (the sink adds no seq/ts — the * store stamps those). */ type WireEvent = { type: string; [key: string]: unknown; }; export interface UiEventSinkOutput { /** Append to the run's NDJSON file (which also fans out live). */ persist(event: WireEvent): void; /** Fan out live only — never written to disk (coalesced deltas). */ emitLive(event: WireEvent): void; } export declare class UiEventSink { private readonly out; private readonly flushMs; /** Pending delta text per open item, per streamed field (insertion order * preserved so a flush replays fields in first-arrival order). */ private readonly buffers; private readonly timers; /** Last persisted non-streamed shape per item — the `item.updated` * meaningful-change filter. */ private readonly shapes; private lastUsage; private ended; constructor(out: UiEventSinkOutput, flushMs?: number); /** Fold one runner-emitted v2 event into the persist/live outputs. */ handle(event: UiEvent): void; /** * Session close, called by the RunManager exactly once when the v1 session * settles: flush everything, then persist v2 `session.error` (fatal) when * the session failed, and `session.ended` — the v2 counterparts of v1's * `done`/`error`, which the mappers deliberately left to the RunManager. */ sessionEnded(reason: StopReason, errorMessage?: string): void; /** Flush every open coalescer now (turn end, session settle). */ flushAll(): void; private bufferDelta; /** Emit one merged `item.delta` per buffered field of the item, live only. */ private flushItem; private persist; private emitLive; } export {};