import type { ControlPlaneClientRecord } from '../runtime/store/domains/control-plane.js'; import type { AnyRuntimeEvent, RuntimeEventDomain, RuntimeEventEnvelope } from '../runtime/events/index.js'; import type { ControlPlaneClientDescriptor } from './types.js'; import type { ControlPlaneServerConfig } from './types.js'; export declare const DEFAULT_SERVER_CONFIG: ControlPlaneServerConfig; export declare const DEFAULT_DOMAINS: readonly RuntimeEventDomain[]; /** * What a client needs to RENDER a turn whose loop it is not running. * * `DEFAULT_DOMAINS` carries `turn`, the text deltas, the turn lifecycle, and * the token usage on `LLM_RESPONSE_RECEIVED`, but it does NOT carry `tools`, * which is where `TOOL_RECEIVED`, `TOOL_EXECUTING`, `TOOL_SUCCEEDED` and * `TOOL_FAILED` are emitted. A subscriber on the defaults therefore receives * everything the model SAID and nothing it DID: the turn renders with its tool * calls missing entirely, which reads as an assistant that paused for no * reason and then answered. * * This is `DEFAULT_DOMAINS` plus `tools`, deliberately ADDITIVE, a stream * moved onto it keeps every domain it already delivered and gains the tool * frames, so no existing consumer loses an event it was relying on. */ export declare const RENDER_GRADE_SESSION_DOMAINS: readonly RuntimeEventDomain[]; /** How a control-plane stream writes one event to its client. */ type ScopedSend = (event: string, payload: unknown, id?: string) => void; /** Live delivery and replay, filtered to one session by the same rule. */ export interface ScopedSessionDelivery { /** Whether a frame stamped with `envelopeSessionId` belongs on this stream. */ mayDeliver(envelopeSessionId: string | undefined): boolean; /** The same decision applied to replayed traffic, which is already serialized. */ wrapSend(send: ScopedSend): ScopedSend; } /** * Restrict a stream to ONE session's frames. * * A stream opened at a per-session path must not render another session's turn * into that session's transcript. The gateway's `sessionId` option alone has * never filtered delivery, it records which session a client is ABOUT, and * several streams set it while deliberately watching the whole daemon, so * scoping is opted into explicitly and resolved here, in one place, rather than * being written twice and drifting between live delivery and replay. * * Two rules: * * - A frame carrying a DIFFERENT session's id is dropped. * - A frame carrying NO session id is delivered. It makes no claim about * another session, and dropping it would silently strip the daemon-wide * lifecycle traffic these streams have always carried. * * `scopedSessionId` of `undefined` means no scoping: everything is delivered, * which is what a fleet-wide observer wants and what every stream did before. */ export declare function createScopedSessionDelivery(scopedSessionId: string | undefined): ScopedSessionDelivery; export interface ControlPlaneEventReplayScope { readonly clientKind?: string | undefined; readonly clientId?: string | undefined; readonly routeId?: string | undefined; readonly surfaceId?: string | undefined; readonly domains?: readonly RuntimeEventDomain[] | undefined; } export interface ControlPlaneRecentEvent { readonly id: string; readonly event: string; readonly createdAt: number; readonly payload: unknown; } export interface ScopedControlPlaneRecentEvent extends ControlPlaneRecentEvent { readonly replayScope?: ControlPlaneEventReplayScope | undefined; } export interface ControlPlaneReplayClientOptions { readonly clientId?: string | undefined; readonly clientKind?: string | undefined; readonly domains?: readonly RuntimeEventDomain[] | undefined; readonly routeId?: string | undefined; readonly surfaceId?: string | undefined; } export declare function serializeEnvelope(envelope: RuntimeEventEnvelope): Record; export declare function toClientDescriptor(record: ControlPlaneClientRecord): ControlPlaneClientDescriptor; export declare function normalizeRuntimeDomains(domains: readonly RuntimeEventDomain[] | undefined): RuntimeEventDomain[]; export declare function hasReplayScope(scope: ControlPlaneEventReplayScope): boolean; export declare function canReplayEventToClient(event: ScopedControlPlaneRecentEvent, options: ControlPlaneReplayClientOptions): boolean; export declare function stripReplayScope(event: ScopedControlPlaneRecentEvent): ControlPlaneRecentEvent; export declare function pruneDisconnectedClientRecords(clients: Map, now?: number): void; /** * Replay recent traffic to a freshly-connected client. Mirrors live delivery: * canReplayEventToClient applies the kind/route/surface/domain-name filters, and * the EVENT_DOMAIN broadcast-domain filter is applied on top so a domain-narrowed * client is not handed a replayed broadcast (e.g. session-update) for a domain it * did not subscribe to. * * `replayDomains` MUST be the same null-or-set value the caller registered on the * live client (`LiveControlPlaneClient.domains`), null when the client did NOT * opt into narrowing (deliver-all, matching live), a `Set` when it did. Do NOT * derive this from `options.domains` here: by the time options reaches this * function it has already been normalized (empty/undefined domains fall back to * DEFAULT_DOMAINS, which excludes e.g. 'permissions'), so re-deriving from it * would always look "explicit" and silently narrow every default consumer's * replay, the bug this parameter exists to prevent. */ export declare function replayRecentTraffic(recentEvents: readonly ScopedControlPlaneRecentEvent[], send: (event: string, payload: unknown, id?: string) => void, options: ControlPlaneReplayClientOptions, replayDomains: ReadonlySet | null, sinceId?: string): ControlPlaneReplayOutcome; /** What a client's stated stream position turned into. */ export type ControlPlaneReplayOutcome = /** No position was presented, the client is new, so it gets the catch-up window. */ { readonly resume: 'none'; } /** * The position was found. `replayed` counts the records that sat after it, * the replay CANDIDATES. The kind/route/surface/domain filters still apply on * top, so a narrowed client may be sent fewer than this; the number says how * much history was there, not how much this particular client was owed. */ | { readonly resume: 'resumed'; readonly sinceId: string; readonly replayed: number; } /** The position was presented but is no longer in the ring; nothing was replayed. */ | { readonly resume: 'unresolved'; readonly sinceId: string; }; /** * Turn a presented `Last-Event-ID` into the slice of history to re-send. * * `recentEvents` is NEWEST-FIRST, so everything recorded after `sinceId` is * what sits in front of it, replayed oldest-first. * * The case worth being explicit about is an id we cannot find. Event ids are * random per record, not ordered, so an id that is not in the ring cannot be * compared against one that is, there is no "everything after" to compute. * Treating that as "the client has no position" and re-sending the whole * catch-up window is what poisons a live turn: the window still holds the * previous turn's `TURN_COMPLETED`, and a consumer that has never seen that * frame finishes the turn now running and drops the rest of it. A client that * states a position is claiming to have seen history; we either know what came * after it or we do not, and guessing costs a turn that was already paid for. * So an unresolvable position replays NOTHING, and the outcome is returned so * the caller can say so on the wire rather than leave the gap silent. */ export declare function resolveReplayResume(recentEvents: readonly ScopedControlPlaneRecentEvent[], sinceId: string | undefined): ControlPlaneReplayOutcome; export {}; //# sourceMappingURL=gateway-utils.d.ts.map