import type { AgentSession, AgentSessionEvent, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcSessionState, SessionEntry, SessionInfo, SettingsConfig, SessionStats } from '@earendil-works/pi-coding-agent'; import type { Api, ImageContent, Model } from '@earendil-works/pi-ai'; import type { BrokerErrorFrame } from '../../api/dto/broker.js'; /** pi's own RPC types, re-exported unchanged. `broker-protocol` stays the one * import site for the wire union; the shapes themselves are pi's, versioned by * pi, so a pi change is a compile error here instead of silent drift. */ export type { RpcExtensionUIRequest, RpcExtensionUIResponse, RpcSessionState, } from '@earendil-works/pi-coding-agent'; /** Single controller (drives the engine) + N read-only observers (§5.3). */ export type ClientRole = 'controller' | 'observer'; /** The full state a (re)attaching client needs to catch up instantly — the * broker's authoritative in-memory view (design §5.2: get_messages + * get_state + get_session_stats). `messages` is pi's `AgentMessage[]` (the * `session.messages` getter type), kept structural to avoid a deep type * import. */ export interface BrokerSnapshot { messages: AgentSession['messages']; stats: SessionStats; /** pi's OWN `get_state` payload, verbatim — not a mirror. `buildSnapshot` * fills it exactly as pi's `rpc-mode.js` does, from the same getters, so a * (re)attaching viewer's header/footer renders at parity with any pi RPC * client and a pi field addition surfaces as a compile error, not drift. * The single deliberate value divergence (not a shape one): a model-less * session's SDK "unknown" stub is reported as `undefined` rather than as a * fictitious selection. */ state: RpcSessionState; /** Broker-owned display state — deliberately NOT inside `state`, which stays a * pure indexed-access mirror of pi's getters. `ctx.ui.setStatus/setWidget/ * setTitle` are LEVELS exposed only as setters, so the broker retains the last * value per key (broker.ts `displayStatuses`/`displayWidgets`/`displayTitle`). * Carrying it here makes catch-up ATOMIC: one `welcome` fully determines a * viewer's state, instead of `welcome` + N synthetic `display_*` edge frames * replayed after it (which painted blank chrome in the gap and made * `applySnapshot(welcome) === fold(reduce, events)` false by construction). * * Ingest defensively: a broker pinned to a pre-`display` runtime generation is * still live and sends `welcome` without it. */ display: { statuses: Record; widgets: Record; title: string | undefined; }; } export interface HelloFrame { type: 'hello'; role: ClientRole; client_id: string; /** Terminal geometry of a tmux-pane viewer; absent for a browser/headless. */ term?: { cols: number; rows: number; }; } /** Drive the engine — controller only. Map 1:1 to session.prompt/steer/followUp/abort. * `images` carries pasted/attached images to the engine (review M1): pi accepts * `prompt(text,{images})` / `steer(text,images)` / `followUp(text,images)` at * 0.78.1. The wire TYPE lives here; T3/T6 wire the runtime side. The BROKER read * cap (`BROKER_READ_CAPS`) is sized to hold a resizeImage-bounded PNG's base64. */ export interface PromptFrame { type: 'prompt'; text: string; images?: ImageContent[]; } export interface SteerFrame { type: 'steer'; text: string; images?: ImageContent[]; } export interface FollowUpFrame { type: 'follow_up'; text: string; images?: ImageContent[]; } export interface AbortFrame { type: 'abort'; /** Optional correlation token (a one-shot daemon interrupt). When present, * the broker acks once the abort is PROCESSED (`ack{for:'abort', id}` with * `detail` naming what was interrupted: 'turn' | 'bash' | 'idle') — and the * single serialized frame loop means that ack also implies every * earlier-accepted `deliver` was already routed. Absent (a viewer's Esc): * no ack, unchanged behavior. */ id?: string; } /** Interactive delivery from a one-shot server client (crtrd's * `POST /v1/nodes/{id}/messages` with `delivery:'interactive'`). The broker * routes it ITSELF — turn in flight → steer, idle → prompt — because only the * broker authoritatively knows streaming state (viewers track it client-side; * a one-shot client can't). Controller-only, like the other drive frames. * Acked once routed (`ack{for:'deliver', id}`, `detail` = 'prompt' | 'steer') * so the daemon handler can return deterministically. */ export interface DeliverFrame { type: 'deliver'; id: string; text: string; images?: ImageContent[]; } /** Run a `!` bash command — controller only. Maps to `session.executeBash()`, * which runs the command, records a `bashExecution` message in context, and * starts NO agent turn (pi's interactive `!`/`!!` semantics). `command` is the * text AFTER the leading `!`/`!!`; `excludeFromContext` is the `!!` form (output * shown but withheld from the LLM). The broker streams the result back as * `bash_start`/`bash_output`/`bash_end` frames. */ export interface BashFrame { type: 'bash'; command: string; excludeFromContext?: boolean; } /** Controller arbitration (§5.3). */ export interface RequestControlFrame { type: 'request_control'; } export interface ReleaseControlFrame { type: 'release_control'; } /** Detach this client — the engine runs on (distinct from `shutdown`). */ export interface ByeFrame { type: 'bye'; } /** Graceful teardown: dispose the engine + exit the broker (design §3.3, the * `HeadlessBrokerHost.teardown` happy path). A FIRST-CLASS control frame, * distinct from `bye` (which only drops one listener). §5.2's wire table omits * it; the plan reconciles §3.3's "graceful teardown over the socket" by * treating `shutdown` as a client→broker control frame. */ export interface ShutdownFrame { type: 'shutdown'; } /** `setModel(model)` — `/model` (selector resolves → chosen id). `pinned`, * when present, is the durable-pin decision for this change — decided by the * SENDER from the model's RAW pre-normalize token (a caller that must * normalize before sending, e.g. `setModelLive`, computes it from the token * it normalized away). Omitted — the common case, where `model` itself IS the * raw/explicit token (a slash-command arg, or a concrete picker pick) — the * broker derives it from `model`'s own shape. */ export interface SetModelFrame { type: 'set_model'; model: string; pinned?: boolean; } /** Push a custom message into a LIVE engine's session WITHOUT waking it. * * This is what makes a property CLAIM-time rather than mint-time for a warm * spare (see `warm-pool.ts`): a booted engine can still be told things. Both * claim-time deliveries ride it — the node's bearings (`crtr-context`, pushed * immediately so it lands as the session's first entry, exactly where the * session_start injection would have put it) and its situational context * (`crtr-situational-context`, `nextTurn` so it folds into whatever turn comes * next instead of starting one a newborn nobody owns must not run). * * Never triggers a turn. The inbox watcher's own situational send is a separate * path precisely because its job IS to wake the node. */ export interface DeliverCustomMessageFrame { type: 'deliver_custom_message'; customType: string; content: string; /** Omitted → pushed straight onto the message list and persisted. */ deliverAs?: 'nextTurn'; /** Extension-only metadata, not sent to the LLM. Carries the `nodeId` stamp * the bearings idempotency guard matches on across later revives. */ details?: { nodeId: string; }; } /** `cycleModel(direction)` — ctrl+p (forward) / shift+ctrl+p (backward). `direction` * defaults to `'forward'` when absent (back-compat with the original frame). */ export interface CycleModelFrame { type: 'cycle_model'; direction?: 'forward' | 'backward'; } /** `cycleLadder(direction)` — alt+m forward / alt+shift+m backward. Walks the INTERLEAVED model ladder * (anthropic/ultra → openai/ultra → anthropic/strong → openai/strong → …), * starting from the current model, applying each rung's own thinking level. * Distinct from `cycle_model`, which steps the flat registry list. `direction` * defaults to `'forward'` when absent. */ export interface CycleLadderFrame { type: 'cycle_ladder'; direction?: 'forward' | 'backward'; } /** `cycleThinkingLevel()` — shift+tab. Cycles to the next thinking level; the new * level reaches viewers via the relayed `thinking_level_changed` event, so the * broker replies a plain `ack` (no payload). Distinct from `set_thinking_level`, * which jumps to a specific level chosen in the thinking picker. */ export interface CycleThinkingFrame { type: 'cycle_thinking'; } /** `setThinkingLevel(level)` — `/settings` thinking / thinking picker. */ export interface SetThinkingLevelFrame { type: 'set_thinking_level'; level: AgentSession['thinkingLevel']; } /** `clearQueue()` — alt+up (dequeue). Removes ALL queued steering + follow-up * messages and returns them so the viewer can restore them to the editor. A * read-AND-mutate op: carries a correlation `id` and the broker replies with a * `data{ kind:'dequeue' }` frame carrying the cleared messages. */ export interface DequeueFrame { type: 'dequeue'; id: string; } /** `setAutoRetryEnabled(enabled)`. */ export interface SetAutoRetryFrame { type: 'set_auto_retry'; enabled: boolean; } /** `setAutoCompactionEnabled(enabled)` — `/settings`. */ export interface SetAutoCompactionFrame { type: 'set_auto_compaction'; enabled: boolean; } /** `compact(instructions?)` — `/compact`. */ export interface CompactFrame { type: 'compact'; instructions?: string; } /** `runtimeHost.newSession()` + rebind — `/new`. */ export interface NewSessionFrame { type: 'new_session'; } /** `runtimeHost.switchSession(path)` + rebind — `/resume` (selector). */ export interface SwitchSessionFrame { type: 'switch_session'; path: string; } /** `runtimeHost.fork(entryId)` + rebind — `/fork` (selector). */ export interface ForkFrame { type: 'fork'; entryId: string; } /** `setSessionName(name)` — `/name`. */ export interface SetSessionNameFrame { type: 'set_session_name'; name: string; } /** Registered commands + templates + skills, MERGED with BUILTIN_SLASH_COMMANDS * (M9: RPC omits builtins) — drives autocomplete. Broker replies via `ack`. */ export interface GetCommandsFrame { type: 'get_commands'; } /** `navigateTree(targetId, options?)` — `/tree`. Options mirror AgentSession.navigateTree. */ export interface NavigateTreeFrame { type: 'navigate_tree'; targetId: string; options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; }; } /** `reload()` — `/reload`. */ export interface ReloadFrame { type: 'reload'; } /** `exportToHtml(path)` / `exportToJsonl(path)` — `/export`. */ export interface ExportFrame { type: 'export'; path: string; format: 'html' | 'jsonl'; } /** Model-selector data (`/model`, ctrl+l): the full registry, the current model, * which models have auth, the `--models` scoped set, and the enabled-model set. */ export interface ListModelsFrame { type: 'list_models'; id: string; } /** Session-resume data (`/resume`): the session list. `scope` selects the * cwd-local list (default) or the cross-project list — `SessionSelectorComponent` * uses BOTH loaders, so the viewer issues one request per scope. */ export interface ListSessionsFrame { type: 'list_sessions'; id: string; scope?: 'cwd' | 'all'; } /** Session-tree + fork data (`/tree` nav AND `/fork` selector): the full tree, * the current leaf, and the prior user messages the fork picker lists. */ export interface GetTreeFrame { type: 'get_tree'; id: string; } /** Interactive settings-menu data (`/settings`): the full `SettingsConfig` toggle * set plus auto-retry + current model (design §5 Unit A). */ export interface GetSettingsFrame { type: 'get_settings'; id: string; } /** Scoped-models picker data (`/scoped-models`): every model + the enabled set. */ export interface ListScopedModelsFrame { type: 'list_scoped_models'; id: string; } /** Inline memory-reference inventory (design: inline memory references §wire * protocol): the full ref inventory metadata a viewer needs to highlight and * complete `/name` / `/scope:name` tokens. Observer-safe, like `get_commands` — * any client may request it, and it is kept SEPARATE from `get_commands` so refs * can never enter leading command completion. */ export interface ListMemoryRefsFrame { type: 'list_memory_refs'; id: string; } /** Answer a blocking extension dialog — pi's public RPC response type. */ export type ExtensionUIResponseFrame = RpcExtensionUIResponse; /** Clone the current session to a new branch (`/clone`). Controller-only. * Broker handler: reads current leaf from sessionManager, creates a branched * session file, switches to it via runReplacement. */ export interface CloneFrame { type: 'clone'; } /** Share the session as a secret GitHub gist (`/share`). Controller-only. * Broker handler: exports session to a temp HTML file, shells `gh gist create --secret`, * returns the URL in ack.detail. */ export interface ShareFrame { type: 'share'; } /** Reload credentials + refresh model registry after a viewer-side auth change. * Open to any client — reload_auth is an idempotent local re-read that doesn't * steer the conversation, so the daemon's canvas-wide fan (one /login → every * live broker) can trigger it without claiming controller and demoting an * attached human. Broker handler: services.authStorage.reload() + * services.modelRegistry.refresh(). */ export interface ReloadAuthFrame { type: 'reload_auth'; } /** Read live source coordinates for an ask-time Humanloop fork. This is an * observer-safe getter only: it never creates a branch, switches a session, or * changes the source manager. */ export interface GetHumanForkCoordinatesFrame { type: 'get_human_fork_coordinates'; id: string; } export type ClientToBroker = HelloFrame | PromptFrame | SteerFrame | FollowUpFrame | AbortFrame | DeliverFrame | BashFrame | RequestControlFrame | ReleaseControlFrame | ByeFrame | ShutdownFrame | SetModelFrame | DeliverCustomMessageFrame | CycleModelFrame | CycleLadderFrame | CycleThinkingFrame | SetThinkingLevelFrame | DequeueFrame | SetAutoRetryFrame | SetAutoCompactionFrame | CompactFrame | NewSessionFrame | SwitchSessionFrame | ForkFrame | SetSessionNameFrame | GetCommandsFrame | NavigateTreeFrame | ReloadFrame | ExportFrame | ListModelsFrame | ListSessionsFrame | GetTreeFrame | GetSettingsFrame | ListScopedModelsFrame | ListMemoryRefsFrame | ExtensionUIResponseFrame | CloneFrame | ShareFrame | ReloadAuthFrame | GetHumanForkCoordinatesFrame; export interface WelcomeFrame { type: 'welcome'; snapshot: BrokerSnapshot; role: ClientRole; controller_id: string | null; /** A dialog already in-flight when this client attached (Phase 4 * attach-mid-dialog). The field exists now; it is only ever populated in * Phase 4 — Phase 3 always sends it absent/null. */ pending_dialog?: RpcExtensionUIRequest | null; /** The pi agent dir (`~/.pi/agent`) from the broker's process — so the viewer * can construct an AuthStorage/ModelRegistry pointing at the SAME auth.json. * crtr surface attach is tmux-local only: broker + viewer share a filesystem. */ agentDir?: string; } export interface ControlChangedFrame { type: 'control_changed'; controller_id: string | null; } /** Broadcast to EVERY client after a successful `set_model`/`cycle_model`. pi * emits no AgentSessionEvent for a model switch, so without this the new model * reaches no viewer at all (the requester gets only a bare ack) and footers * show the stale model until the next unrelated event. The FRAME is ours (the * multiplexing delta: with one client the ack IS the notification, with five * the other four learn nothing); the PAYLOAD is pi's — `Model`, exactly what * pi's own `set_model` response carries and what `snapshot.state.model` holds. * Undefined when the engine has none. */ export interface ModelChangedFrame { type: 'model_changed'; model: Model | undefined; /** The full resolved `provider/id[:thinking]` spec (`formatModelSpec`) — what * the broker persists into the node's own launch recipe. Viewers use it to * persist a non-portable explicit pick as a kind DEFAULT * (`persistDefaultKindModel`) without guessing the provider from the bare * id. Null when the engine has no model. Optional so older brokers' * frames still parse. */ spec?: string | null; } /** The `error` control frame. SOURCED from the shared `@north-light/crouter-api` * contract (`src/api/dto/broker.ts`, the same zero-runtime-dep `/v1` module * Northlight Core imports) so the wire shape here and the shape Core consumes * are one compiler-checked type — no hand-mirror to drift. `{ code, message, * id? }`; `id` is the correlation token echoed ONLY on the failure of a * correlated request (a read-op or `dequeue`, which carry an `id` and otherwise * resolve via a `data` frame) so the viewer can reject the exact pending-by-id * promise instead of hanging it — absent on uncorrelated errors (engine drive * errors, command-op failures, frame_overflow). There is NO `retryable` field. */ export type ErrorFrame = BrokerErrorFrame; /** Result of a controller command op (§1.3): `for` echoes the op name, `ok` the * outcome, `detail` an optional human-readable note. */ export interface AckFrame { type: 'ack'; for: string; ok: boolean; detail?: string; /** Correlation token echoed from an id-bearing request (`deliver`, an * id-carrying `abort`) so a one-shot server client can match the ack to the * exact frame it sent. Absent on uncorrelated acks (unchanged). */ id?: string; } /** A `!` run has begun — viewers create the bash component. */ export interface BashStartFrame { type: 'bash_start'; command: string; excludeFromContext?: boolean; } /** A streamed chunk of combined stdout+stderr for the in-flight `!` run. */ export interface BashOutputFrame { type: 'bash_output'; chunk: string; } /** pi's `BashResult` — reached through `executeBash`'s return type because pi * does not export the interface from its package root. */ type BashResult = Awaited>; /** The `!` run finished — viewers mark the bash component complete. This IS pi's * `BashResult` minus the (already-streamed) `output`, indexed off pi rather than * hand-copied, so a pi field addition is a compile error instead of drift. */ export type BashEndFrame = Omit & { type: 'bash_end'; }; /** `provider/id` model reference — the wire form of a current/selected model. */ export interface WireModelRef { provider: string; id: string; } /** A `--models` scoped cycle entry (mirror of `AgentSession.scopedModels[n]`). */ export interface WireScopedModel { model: Model; thinkingLevel?: AgentSession['thinkingLevel']; } /** Wire form of pi's `SessionInfo`: `created`/`modified` are ISO strings (pi's * are `Date`, which JSON cannot round-trip), and `allMessagesText` is a bounded * opening/latest search excerpt rather than the unbounded transcript text. The * viewer revives the dates before handing them to `SessionSelectorComponent`. */ export interface WireSessionInfo extends Omit { created: string; modified: string; } /** Wire form of pi's `SessionTreeNode` (NOT re-exported from the pi package root, * so mirrored structurally here). Byte-compatible with `SessionManager.getTree()`'s * return; `SessionEntry` IS re-exported, so it is referenced directly. */ export interface WireSessionTreeNode { entry: SessionEntry; children: WireSessionTreeNode[]; label?: string; labelTimestamp?: string; } /** A prior user message for the `/fork` selector (`UserMessageSelectorComponent` * consumes `{ id, text, timestamp? }`). `id` is the session entry id. */ export interface WireForkPoint { id: string; text: string; timestamp?: string; } /** The interactive settings-menu state. The full pi `SettingsConfig` (every toggle * the `/settings` menu shows) PLUS auto-retry and the current model, which the * menu reflects but `SettingsConfig` omits. Build-time `extends` enforces that the * broker populates every `SettingsConfig` field (no thin payload — design R1). */ export interface WireSettings extends SettingsConfig { /** `AgentSession.autoRetryEnabled`. */ autoRetry: boolean; /** `AgentSession.model`, as a `provider/id` ref (null if none selected). */ model: WireModelRef | null; } /** Metadata for one resolvable inline memory reference (design: inline memory * references). `name` is the canonical name a token resolves to (e.g. `'dev'`, * `'taste/foo'`) and what `crtr memory read ` expects; `scope` is the * WINNING scope after first-wins-by-name precedence dedup (display only — * resolution is by `name` alone). No body/source path: this is a metadata-only * inventory, never the document content. */ export interface RefMeta { name: string; kind: 'knowledge' | 'preference'; scope: 'node' | 'project' | 'profile' | 'user' | 'builtin'; shortForm: string; } export interface ListModelsData { type: 'data'; id: string; kind: 'list_models'; /** Every registered model (`ModelRegistry.getAll()`). */ models: Model[]; /** The currently selected model, or null. */ current: WireModelRef | null; /** `provider/id` of every model that has auth configured (`getAvailable()`). */ availableIds: string[]; /** The `--models` scoped cycle set (may be empty). */ scopedModels: WireScopedModel[]; /** The enabled-model patterns (`SettingsManager.getEnabledModels()`), or null. */ enabledModelIds: string[] | null; } export interface ListSessionsData { type: 'data'; id: string; kind: 'list_sessions'; scope: 'cwd' | 'all'; sessions: WireSessionInfo[]; /** The live session's file path, so the picker can mark the current entry. */ currentSessionFile: string | undefined; } export interface GetTreeData { type: 'data'; id: string; kind: 'get_tree'; tree: WireSessionTreeNode[]; currentLeafId: string | null; /** Prior user messages for the `/fork` selector. */ forkPoints: WireForkPoint[]; } export interface GetSettingsData { type: 'data'; id: string; kind: 'get_settings'; settings: WireSettings; } export interface ListScopedModelsData { type: 'data'; id: string; kind: 'list_scoped_models'; /** Every registered model (`ModelRegistry.getAll()`), to enable/disable. */ allModels: Model[]; enabledModelIds: string[] | null; } /** The inline memory-reference inventory reply (design: inline memory * references §wire protocol). Metadata only — no document bodies or source * paths — mirroring `ListModelsData`'s shape for a correlated read-op. */ export interface ListMemoryRefsData { type: 'data'; id: string; kind: 'list_memory_refs'; refs: RefMeta[]; /** Every leading-token string (design: inline memory references, review * Major 6) this session's engine independently consumes as a leading * command/skill/template dispatch — extension commands (bare invocation * name), skills (`skill:`), and prompt templates (bare name) — * exactly the three sources `isLeadingEngineCommand` checks broker-side, * gathered as a full enumeration. Viewers combine this with their OWN * locally-consumed command names to compute leading-command suppression * that matches the broker's actual dispatch boundary; it must never be * merged into `refs` or into command completion. Reconstruct the leading * match as `/${token}` (mirrors the broker's own leading-token * extraction). */ engineLeadingCommandNames: string[]; } /** The `dequeue` result: the steering + follow-up messages just cleared, for the * viewer to restore to the editor. */ export interface DequeueData { type: 'data'; id: string; kind: 'dequeue'; steering: string[]; followUp: string[]; } /** The pure ask-time source coordinate. `modelSpec` always includes its * thinking suffix (`:off` when thinking is disabled), so a future fork can use * the exact provider/model choice without inferring a default. */ export interface HumanForkCoordinatesData { type: 'data'; id: string; kind: 'human_fork_coordinates'; sourceFile: string; sourceSessionId: string; leafId: string; modelSpec: string | null; } /** All correlated data replies (read-ops + dequeue). Discriminate on `kind`. */ export type BrokerDataFrame = ListModelsData | ListSessionsData | GetTreeData | GetSettingsData | ListScopedModelsData | ListMemoryRefsData | DequeueData | HumanForkCoordinatesData; /** An extension UI call routed to viewers — pi's public RPC request type. */ export type ExtensionUIRequestFrame = RpcExtensionUIRequest; /** Broker → client: dismiss a previously-forwarded blocking dialog by request id, * WITHOUT a client answer. The broker sends this exactly when it resolves a pending * dialog ITSELF rather than the client answering it — the extension aborted the * request out-of-band (e.g. a local OAuth loopback callback won the race against a * still-open manual-paste dialog) or the broker-side timeout fired. The client tears * down ONLY the overlay whose `id` matches; every other dialog stays exactly as it * was, so an unrelated request can never dismiss an unrelated blocking dialog. */ export interface ExtensionUIDismissFrame { type: 'extension_ui_dismiss'; id: string; } /** Everything the broker can send. Live `AgentSessionEvent`s are relayed * verbatim (the broker adds nothing); the broker's own control frames carry * non-colliding `type` discriminants. */ export type BrokerToClient = WelcomeFrame | ControlChangedFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | AgentSessionEvent; /** Encode one frame as a single newline-terminated JSON line. */ export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string; /** Byte bounds for a {@link FrameDecoder} (C5). */ export interface FrameDecoderCaps { /** Largest single (unterminated) frame the decoder will buffer before throwing. */ maxLineBytes: number; /** Largest the internal buffer may peak to in one push before throwing. */ maxTotalBytes: number; } /** Thrown by {@link FrameDecoder.push} when a peer's buffered bytes exceed a cap * (C5). The caller catches it, best-effort sends `error{code:'frame_overflow'}`, * and DESTROYS that socket — cap-and-drop the peer, never grow-to-OOM. `kind` * says which bound tripped: a single oversized frame (`line`) or total backlog * (`total`). */ export declare class FrameOverflowError extends Error { readonly kind: 'line' | 'total'; readonly bytes: number; readonly cap: number; constructor(kind: 'line' | 'total', bytes: number, cap: number); } /** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the * full message history and can be many MiB, so these are generous. Covers * realistic long sessions; snapshot CHUNKING is deferred (plan §1.1 known * limitation, review m3 — the long-lived broker makes a big welcome a realistic, * not pathological, trigger, but the fixed cap is accepted for Phase 4). */ export declare const CLIENT_READ_CAPS: FrameDecoderCaps; /** Caps the BROKER uses reading CLIENT frames. Plan §1.1 specified a tight * 4/16 MiB, but review M1 requires image-paste frames to fit: a resizeImage- * bounded PNG's base64 is a few MiB and would clip a 4 MiB line cap. So these are * RAISED above the plan's 4/16 to hold an image-bearing `prompt`/`steer`/ * `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame * is cap-and-dropped, never grow-to-OOM (C5). */ export declare const BROKER_READ_CAPS: FrameDecoderCaps; /** Incremental newline-delimited JSON reader: feed raw socket chunks, get back * the complete frames decoded so far. Buffers a partial trailing line across * pushes; silently drops a malformed (unparseable) line (a viewer must never * crash the broker on bad JSON). The caller narrows the `unknown` against the * frame unions. * * BOUNDED (C5): an oversized buffer is the THROW case. If a single push peaks the * internal buffer above `maxTotalBytes`, or the surviving (unterminated) partial * line exceeds `maxLineBytes`, {@link push} throws {@link FrameOverflowError} * instead of returning frames — the caller drops that peer rather than letting a * malicious/buggy stream grow the buffer to OOM. Bytes are measured with * `Buffer.byteLength`, never string length; a running counter makes each push's * size CHECK O(1) (no re-scan of the whole buffer to measure it). * * AMORTIZED O(total bytes) (perf regression fix, 2026-06-09): the partial line * carried across pushes is held as an ARRAY of chunk strings (`parts`), joined * only when its terminating `\n` arrives. The `\n` scan looks at each incoming * chunk ONCE — never re-scanning the accumulated carry — so a multi-MiB frame * arriving in ~64 KiB socket chunks costs O(frame) total instead of O(frame × * chunks). The old `buf += chunk; buf.indexOf('\n')` shape flattened + re-walked * the whole carry per chunk: a 16 MiB welcome snapshot cost ~250 ms of * event-loop stall in `crtr surface attach` (typing lag) and the same again in the * broker; this shape decodes it in ~13 ms. * * A `StringDecoder` decodes incoming Buffer chunks: an incomplete trailing * multibyte sequence is held back across pushes instead of being mangled to * U+FFFD, so a char split at a `node:net` chunk boundary is never corrupted, and * `partBytes` counts decoded-string bytes (a raw `chunk.length` would desync * and weaken the C5 cap on multibyte/invalid input). */ export declare class FrameDecoder { private readonly caps; /** The unterminated partial line carried across pushes, as unjoined chunks * (never contains a '\n'). Joined lazily when its newline arrives. */ private parts; /** Byte length (Buffer.byteLength) of `parts` joined — kept in exact lockstep. */ private partBytes; private readonly utf8; constructor(caps: FrameDecoderCaps); push(chunk: Buffer | string): unknown[]; }