import { EventEmitter } from 'node:events'; import type { NodeRow } from '../canvas/types.js'; import type { BrokerDataFrame, BrokerToClient, ClientToBroker, DequeueFrame, GetHumanForkCoordinatesFrame, GetSettingsFrame, GetTreeFrame, ListMemoryRefsFrame, ListModelsFrame, ListScopedModelsFrame, ListSessionsFrame } from '../runtime/broker-protocol.js'; import { type Transport } from './transport.js'; export { BrokerUnavailableError, type Transport } from './transport.js'; export { SocketTransport } from './transport-socket.js'; export { RelayTransport, type RelayTarget } from './transport-relay.js'; /** A correlated read-op (or `dequeue`) request MINUS the client-chosen `id` — * {@link BrokerClient.request} mints the `id`, sends the frame, and resolves * with the matching `data` reply. The picker/operator code builds these; the * client owns the correlation token so callers never hand-roll one. */ export type ReadOpRequest = Omit | Omit | Omit | Omit | Omit | Omit | Omit | Omit; export interface BrokerClientOptions { /** Where the frames go. Defaults to a {@link SocketTransport} against this * node's local `view.sock` — the common case. Pass a {@link RelayTransport} * to attach a remote node instead; every method behaves identically either * way. */ transport?: Transport; /** Re-emit a `data` frame that matches no in-flight request as a generic * `frame` event. OFF by default: the attach viewer's frame router would * mis-handle a stray `data` frame as a pi `AgentSessionEvent`. The operator * view (`core/view/stream-local.ts`) passes read-op replies through to its * own consumer and is the one caller that needs `true`. */ emitUnmatchedDataFrames?: boolean; } /** The reconnect supervisor's give-up predicate (extracted pure so it is * testable without a socket or TUI). After a broker close the viewer KEEPS * re-dialing the same target while the node is still alive — a yield * leaves `status='active'` (intent='refresh') and the daemon revives a fresh * broker on the same path. It gives up only when the node is genuinely gone: * a terminal status (done/dead/canceled) or a reaped row (null). `idle` is NOT * terminal — an idle-release node revives on its next inbox wake, so keep * trying (the supervisor waits at a relaxed cadence for as long as the row * stays live). Local canvas rows only — a remote target has no local row, so * callers driving a remote attach must not consult this (see viewer.ts). */ export declare function reconnectShouldGiveUp(row: NodeRow | null): boolean; /** The plan-fixed interface (Wave 3): `connect()`, `on('frame', …)`, * `send(frame)`, `on('close', …)`, plus `connect`/`error` events. */ export interface BrokerClient { on(event: 'connect', listener: () => void): this; on(event: 'frame', listener: (frame: BrokerToClient) => void): this; on(event: 'close', listener: () => void): this; on(event: 'error', listener: (err: Error) => void): this; once(event: 'connect', listener: () => void): this; once(event: 'frame', listener: (frame: BrokerToClient) => void): this; once(event: 'close', listener: () => void): this; once(event: 'error', listener: (err: Error) => void): this; off(event: 'connect', listener: () => void): this; off(event: 'error', listener: (err: Error) => void): this; } export declare class BrokerClient extends EventEmitter { private readonly nodeId; private readonly transport; private readonly emitUnmatchedDataFrames; private closeEmitted; /** Tracks live connect↔close state so {@link request} can fail fast on a * dead/absent connection rather than parking a promise the reply will * never reach (transport-agnostic mirror of the old socket.destroyed * check). A post-close request lands here too — {@link rejectAllPending} * already ran by the time this flips false. */ private connected; /** The last transport-level error message, so a close can reject any * in-flight requests with the precise reason (e.g. an oversized-frame * overflow) instead of a generic one. Cleared on every fresh connect. */ private lastTransportError; /** In-flight correlated read-ops, keyed by the `id` minted in {@link request}. * Resolved by the matching `data` frame / rejected by the matching `error` * frame in {@link onFrame}, the request's timeout, or transport teardown. */ private pending; constructor(nodeId: string, options?: BrokerClientOptions); /** Issue a correlated read-op and resolve with the broker's `data` reply (or * reject on the correlated `error`, a timeout, or transport teardown). Mints * the `id`, sends `{...frame, id}`, and parks a resolver consumed by * {@link onFrame}. The reply is narrowed by the caller on its `kind`. */ request(frame: ReadOpRequest): Promise; /** Reject + clear every in-flight request (connection gone / decode error) * so a picker fetch never hangs past the connection it rode on. */ private rejectAllPending; /** Open the connection. Emits `connect` on success or `error` (a * {@link BrokerUnavailableError} when the target has no reachable broker) * on failure. Idempotent guard is the caller's job — call once. */ connect(): void; /** Re-establish the connection after a broker exit (a yield→revive cycle), * to the SAME target. Resolves on `connect` (caller re-sends `hello`); * rejects on a retryable dial failure. */ redial(): Promise; /** Encode + send one client→broker frame. No-op on a dead/absent * connection; never throws. */ send(frame: ClientToBroker): void; /** Detach: tear down the transport. `close` fires → the caller tears down. */ close(): void; private onFrame; private onClose; /** Emit `error` only when a listener exists — a bare EventEmitter `error` * with no listener throws, and this client must never throw uncaught. */ private emitError; }