Signatures quoted from source. Usage: Bind a Statewire, Deploy a Statewire. Related: Statewire reference.
Entry points
| Import from | Exports |
|---|---|
@assistant-ui/react-statewire | StatewireThread, runsMaterialize, useStatewireSendCommand, useStatewireRuns, useStatewireState |
@assistant-ui/react-statewire/local-storage | LocalStorageSession, LocalStorageComposerDrafts, localStorageSessionBackend, localStorageSessionNamespace |
@statewire/ai-sdk | useChat, plain, isRecord, syncArray, syncRecord, syncIndex, useLatest |
statewire-rivet | statewireActor, StatewireRivet |
@statewire/cloudflare | StatewireDurableObject, statewireHandlers, routeStatewireRequest, getStatewireByName, WS_SUBPROTOCOL |
statewire-node | statewireHost |
@assistant-ui/react-statewire
StatewireThread
const StatewireThread: <T>(options: StatewireThread.Options<T>) => ResourceElement<ClientOutput<"thread">>;Binds a statewire to an assistant-ui thread for AuiConfig({ thread }). The run-dialect commands map onto composer and message actions.
StatewireThread.* | Shape |
|---|---|
ComposerDraft | { readonly text: string; readonly attachments: readonly CompleteAttachment[] } |
ComposerStorage | { read(): ComposerDraft | undefined; write(draft: ComposerDraft): void; clear(): void }; write persists synchronously |
StatewireThread.Options<T>
| Name | Type | Default | Meaning |
|---|---|---|---|
transport | StatewireThread.Transport<T> = ResourceElement<Statewire<T | undefined, any>> | required | Statewire transport |
initialState | T | required | Pre-attach state, replaced by the attach snapshot; runs: [] projects ready, an absent runs mount projects loading until attach |
converter | StatewireThread.StateConverter<T> = (state: T, connectionMetadata: ConnectionMetadata) => State | required | Projects the statewire state into messages |
optimistic? | StatewireClient.Optimistic<T, Record<string, (...args: any[]) => unknown>> | — | Folds app-owned keys for an unsettled command; runs after the built-in run-dialect reducer |
materialize? | StatewireClient.Materialize<NonNullable<T>> | — | Makes dispatch records concrete in the draft; without it, dispatch limbo stays out of messages |
contextSync? | "command-send" | — | Mirrors the ambient model context (system, tools, callSettings, config) through native lease context on the application protocol; the latest object is restored automatically on every attachment and replaced on change; throws statewire: contextSync requires a subscribable model context without one |
stopPayload? | () => StopPayload | — | run/stop payload, merged under runId and epoch; must return a plain object. The verdict's retracted items are dequeued (run/dequeue) into an empty composer, joined by blank lines; a rejected dequeue leaves the item queued |
onCommandChange? | (update: Statewire.CommandUpdate<Record<string, (...args: any[]) => unknown>>, ctx: { state: NonNullable<T> }) => unknown | — | Observes each command lifecycle step; runs after the built-in pending-command retirement. Its return value passes through to the client, so a returned promise defers a durable settled update's ack (Settled updates) |
onStateChange? | (state: NonNullable<T>, ctx: { changes: Statewire.Changes }) => void | — | Fires with the attach snapshot, then once per change |
onConnectionChange? | (connection: StatewireClient.Connection) => void | — | Connection transitions, starting with the initial status |
onError? | (error: Error) => void | — | Receives rejections of dispatched commands and of run/stop, and composer-restore attachment failures |
capabilities? | { edit?: boolean; continue?: boolean; reload?: boolean; unstable_switchBranch?: boolean } | — | Enables message edit, run/continue, run/reload, and message/switch-branch affordances |
storage? | { composer?: ResourceElement<ComposerStorage>; session?: ResourceElement<StatewireClient.SessionStorage> } | — | Composer drafts restore on mount, mirror every change, clear when consumed; the session holds the lane identity and un-durable commands |
adapters? | { attachments?: AttachmentAdapter; speech?: SpeechSynthesisAdapter; feedback?: FeedbackAdapter } | — | assistant-ui adapters |
State and runs
StatewireThread.* | Shape |
|---|---|
State | { readonly messages: readonly ThreadMessage[]; readonly state?: ReadonlyJSONValue }; app-owned keys only; a returned state replaces the replicated state on Extras.state and the thread |
ConnectionMetadata | { pendingCommands: Command[]; isSending: boolean; connection: StatewireClient.ConnectionState; runs: Runs; documents: StatewireDocuments.Source | undefined; mounted: ReadonlyMap<number, StatewireDocuments.Document>; invoke: (protocol: string, method: string, params: readonly unknown[]) => Promise<unknown> }; local transports are always connected; documents and invoke keep their identity across passes, mounted is a new map whenever a document changes |
StopPayload | { readonly reason?: string; readonly [key: string]: ReadonlyJSONValue | undefined } |
Extras | { [symbolStatewireExtras]: true; sendCommand: (command: Command) => Promise<unknown>; reconnect: () => void; connection: StatewireClient.ConnectionState; documents: StatewireDocuments.Source | undefined; invoke: (protocol: string, method: string, params: readonly unknown[]) => Promise<unknown>; state: unknown; status: ThreadStatus; isBusy: boolean; inputRequests: readonly InputRequestState[]; runs: Runs; outbox: StatewireClient.Outbox }; outbox releases, edits or removes held sends (Outbox) |
RunStatus | "running" | "stopped" | "error" | "input-required" |
ThreadStatus | "loading" | "ready" | "evicted" | "disconnected" | RunStatus; evicted/disconnected are client-derived |
RunQueueItem | { readonly id: string; readonly parts: readonly (TextPart | FilePart)[]; readonly metadata?: Record<string, unknown> } |
RunsQueueEntry | UserMessage & { readonly meta?: ReadonlyJSONValue }; meta is host-stamped |
RunsHeldEntry | RunsQueueEntry & { readonly localMeta: unknown }; a held send folded from the outbox |
RunsInputOutcome | { readonly request: InputRequest; readonly response: InputResponse | null; readonly meta: ReadonlyJSONValue | null }; null response is abandoned |
RunsStopping | { readonly reason: "stop" | "message-edit" | "message-reload" }; message-edit/message-reload pair with nextDispatch |
RunsError | { readonly message: string; readonly [key: string]: ReadonlyJSONValue | undefined }; set on an unhandled failure, cleared by the next user-initiated run |
RunsState | { readonly status: "ready" | RunStatus; readonly runs: readonly RunState[] }; status projects runs[0]?.status ?? "ready", at most one entry |
Runs | RunState minus status, plus readonly held: readonly RunsHeldEntry[]; runs[0] with unsettled commands folded in, resting defaults while no entry is live. held lists held run/send and run/edit sends carrying a message, in queue order, with the entry's localMeta; they never fold in as a run. Release or remove them through Extras.outbox |
MaterializeEffects | { append: (message: RunsQueueEntry) => void; rollback?: (to: string | null) => void }; rollback required for hosts that dispatch edits or reloads |
BranchMetadata | { readonly id: number; readonly siblings: readonly number[] }; read from metadata.custom.branch, siblings in creation order, own id included |
StatewireThread.RunState, one element of the mount's runs array:
| Name | Type | Meaning |
|---|---|---|
runId | string | null | Client-minted, adopted by the host at run creation; persists through steers, input-required, and error/stop; retires when the queue drains and the run completes |
epoch? | number | Counts the entry's dispatches; a run/stop carrying a stale epoch rejects |
status | RunStatus | Run subsystem status |
queue | readonly RunsQueueEntry[] | Queued user messages |
steerQueue | readonly RunsQueueEntry[] | Steer-lane messages |
dispatching? | RunsDispatch | The unapplied batch handed to the run; cleared at applied |
stopping? | RunsStopping | Present while a stop of the live run is underway |
nextDispatch? | RunsDispatch | The complete dispatch staged behind a stopping rewind; promotes under the entry's runId |
inputRequests? | readonly InputRequestState[] | The parked pending set; present only in input-required |
error? | RunsError | Set only by an unhandled throw; cleared by the next user-initiated run |
StatewireThread.RunsDispatch, the dispatch record; absent between applied and the next dispatch or take:
| Name | Type | Meaning |
|---|---|---|
trigger | "message-send" | "message-edit" | "message-reload" | "input-resume" | "error-continue" | "stop-continue" | "steer" | What started the dispatch |
messages | readonly RunsQueueEntry[] | The dispatched batch |
rollbackTo? | string | null | Present on rewind dispatches: truncate the thread at this id first; null names the root |
rootMeta? | ReadonlyJSONValue | The run-starting op's meta; present only when carried |
inputOutcomes? | readonly RunsInputOutcome[] | Input outcomes riding the dispatch; visible until applied |
Commands and input requests
StatewireThread.* | Shape |
|---|---|
TextPart, FilePart | { readonly type: "text"; readonly text: string }, { readonly type: "file"; readonly mediaType: string; readonly url: string; readonly filename?: string } |
UserMessage | { readonly id: string; readonly role: "user"; readonly parts: readonly (TextPart | FilePart)[]; readonly metadata?: Record<string, unknown> } |
Placement | { readonly insertAfter?: string | null; readonly insertBefore?: string | null }; absent is default placement, null a queue boundary |
RunSendBehavior | "auto" | "steer" | "preempt" | "queue" |
RunSendCommand | { readonly type: "run/send"; readonly runId: string; readonly behavior?: RunSendBehavior } & ({ readonly message: UserMessage; readonly runAnchorMessageId?: string | null } | { readonly messageId: string }) & Placement; behavior defaults to "auto"; messageId moves a queued message into the behavior's lane; runAnchorMessageId is present on run-creating sends, null asserts an empty thread |
RunDequeueCommand | { readonly type: "run/dequeue"; readonly runId: string; readonly messageId: string } |
RunEditCommand | { readonly type: "run/edit"; readonly runId: string; readonly message: UserMessage; readonly sourceId: string; readonly runAnchorMessageId: string | null }; runAnchorMessageId asserts sourceId's parent, null names the root |
RunReloadCommand | { readonly type: "run/reload"; readonly runId: string; readonly sourceId: string; readonly runAnchorMessageId: string | null } |
RunContinueCommand | { readonly type: "run/continue"; readonly runId?: string } |
RunInputCommand | { readonly type: "run/input"; readonly runId?: string; readonly requestId: string; readonly response: InputResponse } |
SwitchBranchCommand | { readonly type: "message/switch-branch"; readonly branchId: number } |
RunCommand | union of the six run/* commands |
Command | RunCommand | UserCommands (Assistant.Commands values) |
ToolCallInputRequest, ToolApprovalInputRequest | { readonly type: "tool-call" | "tool-approval"; readonly id: string; readonly toolCallId: string } |
CustomInputRequest | { readonly type: string; readonly id: string; readonly [key: string]: ReadonlyJSONValue | undefined } |
InputRequest | ToolCallInputRequest | ToolApprovalInputRequest | CustomInputRequest; open union for custom types |
InputResponse | ReadonlyJSONValue |
InputRequestState | InputRequest & { readonly response?: InputResponse; readonly meta?: ReadonlyJSONValue }; a present response key marks it answered, meta is the answer's run/input meta |
runsMaterialize, useStatewireSendCommand, useStatewireRuns, useStatewireState
const runsMaterialize: (mount: unknown, effects: StatewireThread.MaterializeEffects) => void;
const useStatewireSendCommand: () => (command: StatewireThread.Command) => Promise<unknown>;
const useStatewireRuns: () => StatewireThread.Runs;
function useStatewireState(): UserExternalState;
function useStatewireState<T>(selector: (state: UserExternalState) => T): T;runsMaterialize is the run dialect's materialize fold: rollback(rollbackTo) first when present, then append per dispatched message. It throws when the record carries rollbackTo and no rollback effect. The hooks and every Extras read throw This method can only be called when you are using StatewireThread off another thread type. The hooks, inside the AuiProvider: dispatch any command (rejects with StatewireSendError), the typed runs read, and the replicated statewire state (UserExternalState is Assistant.ExternalState's value type, Record<string, unknown> when none is declared).
@assistant-ui/react-statewire/local-storage
LocalStorageSession, LocalStorageComposerDrafts
const LocalStorageSession: (options: LocalStorageSession.Options) => ResourceElement<StatewireClient.SessionStorage>;
const LocalStorageComposerDrafts: (options: LocalStorageComposerDrafts.Options) => ResourceElement<StatewireThread.ComposerStorage>;Both options require threadId: string and a nonempty stable principal: string. Changing principal on a mounted resource throws; remount the thread/session per account. Session options also accept protocol, retention, limits, onError, onEvict, and migrate from the key-value session adapter; draft options accept ttl and max. Tabs sharing a principal and threadId share the record (clientId, per-lane nextSeq, commands, outbox, parked verdicts, undelivered inbox updates): one client id, one lane, verdicts fanned out to every attach (concurrent attaches). Record writes serialize under a Web Lock.
LocalStorageSessionstores theSessionRecordunderaui:statewire-session:<encoded-principal>:<encoded-threadId>.locktakes the Web Lockaui:statewire-session-lock:<encoded-principal>:<encoded-threadId>,notifyLockthe Web Lockstatewire-notify:<storage key>,subscriberides thestorageevent. Throws withoutlocalStorageornavigator.locksLocalStorageComposerDraftsstores underaui:composer-draft:<encoded-principal>:<encoded-threadId>. The mount sweep, across the current principal's keys, drops drafts older thanttl(default 7 days) and beyond the newestmax(default 50); a corrupt entry is removed, an empty draft deletes its key. Throws withoutlocalStorage- Legacy unscoped records remain quarantined. Migration requires explicit ownership attribution.
localStorageSessionBackend, localStorageSessionNamespace
const localStorageSessionBackend: KeyValueSessionStorage.Backend;
const localStorageSessionNamespace: string;The backend LocalStorageSession writes through and the namespace it stores sessions under. Pass both, with the principal, to readOutboxes / subscribeOutboxes to list every thread's outbox:
const unsubscribe = subscribeOutboxes(
localStorageSessionBackend,
{ namespace: localStorageSessionNamespace, principal: userId },
(outboxes) => setPending(outboxes.flatMap((o) => o.entries)),
);@statewire/ai-sdk
useChat
resumeStream() is currently a no-op; the resume option is unused.
const useChat: (options?: UseChatOptions) => UseChatHelpers;AI SDK useChat hosted in an in-process statewire. The ChatTransport streams UIMessageChunks into ChatState.
- The host element is internal.
ChatCommandsis the whole command set; extra commands need your own host UseChatOptionscarries neitheroptimisticnormaterializetransportdefaults tonew DefaultChatTransport(), read fresh per request- Chunk handling: the chunk pipeline
| Type | Shape |
|---|---|
UseChatOptions | { id?: string; transport?: ChatTransport<UIMessage>; messages?: UIMessage[]; messageMetadataSchema?: FlexibleSchema<unknown>; dataPartSchemas?: UIDataPartSchemas; generateId?: IdGenerator; onToolCall?; sendAutomaticallyWhen?: (options: { messages: UIMessage[] }) => boolean | PromiseLike<boolean>; onFinish?; onError?; onData?; throttle?: number; resume?: boolean } |
UseChatHelpers | Statewire.CommandsProxy<ChatCommands> & { id: string; messages: UIMessage[]; status: ChatStatus; error: { message: string } | undefined; addToolResult: Statewire.CommandsProxy<ChatCommands>["addToolOutput"] }: addToolResult aliases addToolOutput |
ChatCommands | { sendMessage(message?: UseChatSendMessage | null, options?: ChatRequestOptions): void; regenerate(options?: { messageId?: string } & ChatRequestOptions): void; stop(): void; clearError(): void; resumeStream(): void; setMessages(messages: UIMessage[] | ((messages: UIMessage[]) => UIMessage[])): void; addToolOutput(params: ChatAddToolOutputParams): void; addToolApprovalResponse(params: ChatAddToolApprovalResponseParams): void } |
UseChatSendMessage | (CreateUIMessage<UIMessage> & { text?: never; files?: never; messageId?: string }) | { text: string; files?: FileList | FileUIPart[]; metadata?: unknown; parts?: never; messageId?: string } | { files: FileList | FileUIPart[]; metadata?: unknown; parts?: never; messageId?: string }: a messageId naming an existing user message truncates to it and replaces it; an unknown or non-user id rejects with StatewireSendError (failure.fate: "rejected"); omitted appends |
resume and resumeStream() are inert. stop() acts only while streaming or submitted, clearError() only in error. regenerate and the tool helpers throw StatewireReject on an empty thread or an unknown messageId.
| ChatState | { id: string; messages: UIMessage[]; status: ChatStatus; error?: { message: string } } |
| ChatAddToolOutputParams | { tool: string; toolCallId: string; options?: ChatRequestOptions } & ({ state?: "output-available"; output: unknown; errorText?: never } \| { state: "output-error"; output?: never; errorText: string }) |
| ChatAddToolApprovalResponseParams | { id: string; approved: boolean; reason?: string; options?: ChatRequestOptions } |
plain
const plain: <T>(value: T) => T;Deep JSON snapshot. Unwraps statewire proxies into plain data.
isRecord, syncArray, syncRecord, syncIndex
const isRecord: (value: unknown) => value is Record<string, unknown>;
const syncArray: (target: unknown[], next: unknown[]) => void;
const syncRecord: (target: Record<string, unknown>, next: Record<string, unknown>) => void;
const syncIndex: (target: unknown[], index: number, next: unknown) => void;Reconcile a full snapshot into target with minimal in-place mutations. syncIndex appends past the end.
useLatest
const useLatest: <T>(value: T) => () => T;Stable getter for the latest render's value.
statewire-rivet
Direct StatewireWebsocket connections through the RivetKit 2.3.10 gateway currently fail during subprotocol negotiation. See deployment status and issue #325.
statewireActor, StatewireRivet
const statewireActor: <TState, TConnParams, TConnState, TVars, TInput, TDatabase, TEvents, TQueues, TActions>(config: statewireActor.Config<…>) => ActorDefinition<…>;
namespace statewireActor {
type Config<…> = ActorConfigInput<…> & StatewireHostInternal.SocketHostOptions & { statewire: ResourceElement<Statewire>; onWebSocket?: never };
}
const StatewireRivet: (transport: ResourceElement<Statewire>, options?: StatewireHostInternal.SocketHostOptions) => StatewireRivet.Instance;statewireActor is a rivetkit actor (peer rivetkit ^2.3.10) serving a statewire host, one socket host per actor instance. It sets canHibernateWebSocket to authorize === undefined, over any value you pass, and rebuilds the host in createVars on every wake. Client ids ride the client query parameter. StatewireRivet is the manual composition into your own actor({...}) via onWebSocket; there, set canHibernateWebSocket: false when passing authorize. Actor destroy sends every client fin: gone, runs your onDestroy, then disposes the host.
StatewireRivet.* | Shape |
|---|---|
Instance | { onWebSocket(c: Context, websocket: WebSocket): void; finish(fin: StatewireHostInternal.Fin): void; dispose(): void } |
Context | { readonly request?: Request } |
WebSocket | StatewireHostInternal.Socket & { readonly readyState: 0 | 1 | 2 | 3; readonly url: string; addEventListener(type, listener): void } |
@statewire/cloudflare
StatewireDurableObject, statewireHandlers
const StatewireDurableObject: <Env = unknown>(transport?: ResourceElement<Statewire> | StatewireDurableObject.TransportFactory<Env>, options?: StatewireDurableObject.Options) => StatewireDurableObject.Class<Env>;
const statewireHandlers: (ctx: StatewireDurableObject.Context, transport: StatewireDurableObject.Transport, options?: StatewireDurableObject.Options) => StatewireDurableObject.Handlers;StatewireDurableObject: class factory for a complete Durable Object (extends StatewireDurableObject(ThreadHost())).- An element is sugar over the overridable
transport()method. A(restored, env) => transportfactory mounts durable. - WebSocket upgrades,
/stream, and/framesare served. Every other request goes to the overridableonRequest(426 by default). - Client ids, the
authorizevalue, and its originalauthorizedAttimestamp survive hibernation viaserializeAttachment. Enabled lease expiry requires a valid unexpired timestamp before restoration; older sockets reconnect and authorize again. A retained socket with a malformed attachment is closed1008. - An open SSE stream holds the object out of hibernation.
- An element is sugar over the overridable
statewireHandlers: raw handlers for an existing Durable Object class, called from the constructor. A durable mount restoresstatewire:snapshotunderblockConcurrencyWhileand persists it on a 1000 ms debounce, flushed on socket close and SSE stream end and written afterfinishanddispose. Client watermarks live understatewire:client:<id>(theClientStoreofStatewireSocketHost); retained sockets' records load underblockConcurrencyWhile. Afterdispose,fetchanswers410and socket events are ignored.
| Name | Type | Default | Meaning |
|---|---|---|---|
heartbeatIntervalMs? | number | 0 | Off: a quiet object hibernates, and a StatewireWebsocket client redials after 30 s of silence, re-attaching with a fresh snapshot. Any positive value holds the socket open with a setInterval while sockets are attached; keep it under the client's 30 s budget |
persistSnapshot? | boolean | true | false keeps the client records durable but never reads or writes statewire:snapshot: the factory receives undefined, and the transport needs no snapshot() or subscribe(). For a transport whose state lives elsewhere, or exceeds the storage value limit |
Setup: Durable Objects.
routeStatewireRequest, getStatewireByName, WS_SUBPROTOCOL
const routeStatewireRequest: (request: Request, env: Record<string, unknown>) => Response | Promise<Response> | undefined;
const getStatewireByName: (binding: StatewireDurableObject.Namespace, name: string) => StatewireDurableObject.Stub;
const WS_SUBPROTOCOL = "statewire.v1";Worker router for /:binding-name/:instance-name (binding match ignores case, -, _; the instance name is URL-decoded; undefined when nothing matches or the path has fewer than two segments) and the direct stub lookup. /frames with a non-POST method answers 405 with Allow: POST. StatewireDurableObject() with neither a transport nor a transport() override throws on first use.
StatewireDurableObject.* | Shape |
|---|---|
WebSocket | StatewireHostInternal.Socket & { serializeAttachment(attachment: unknown): void; deserializeAttachment(): unknown } |
Attachment | { clientId: string; authorized?: boolean; authorizedAt?: number; selection?: StatewireProtocol.Selection; attacher?: string; auth?: unknown }: the serialized attachment format |
Context | { acceptWebSocket(ws): void; getWebSockets(): WebSocket[]; storage?: Storage; blockConcurrencyWhile?<T>(callback: () => Promise<T>): Promise<T> } |
Storage | { get(key: string): Promise<unknown>; put(key: string, value: unknown): Promise<void>; delete(key: string): Promise<boolean>; list(options: { prefix: string }): Promise<Map<string, unknown>> } |
DurableTransport | { snapshot(): unknown; subscribe(listener: () => void): () => void }: required of a factory-mounted transport; build it from getStateHost(state) (example) |
Transport | ResourceElement<Statewire> | ((restored: unknown) => ResourceElement<Statewire>) |
TransportFactory<Env> | (restored: unknown, env: Env) => ResourceElement<Statewire> |
Handlers | { fetch(request): Response | Promise<Response>; webSocketMessage(ws, message: string | ArrayBuffer): void; webSocketClose(ws): void; webSocketError(ws): void; finish(fin: StatewireHostInternal.Fin): void; reconnect(match?: (attach: { clientId: string; auth: unknown }) => boolean, code?: string, options?: { retry?: number }): void; dispose(): void }: reconnect ends every authorized socket whose stored { clientId, auth } match accepts with fin reconnect / code (default reauthorize) and close 1012, without waking the host |
Options | StatewireHostInternal.SocketHostOptions & { persistSnapshot?: boolean } |
Instance<Env> | { ctx: DurableObjectState; env: Env; statewire: Handlers; transport(): ResourceElement<Statewire>; onRequest(request): Response | Promise<Response>; fetch; webSocketMessage; webSocketClose; webSocketError } |
Class<Env>, BaseClass | new (ctx: DurableObjectState, env: Env) => Instance<Env>; new (ctx, env: unknown) => { ctx; env } |
Namespace, Stub | { idFromName(name: string): unknown; get(id: unknown): Stub }; { fetch(request: Request): Response | Promise<Response> } |
With the default lease lifetime, restored sockets missing the original authorizedAt reconnect before state is restored.
statewire-node
statewireHost
const statewireHost: (options: StatewireNode.Options) => StatewireNode.Host;A statewire host over standard Request/Response for Node servers and pinned instances. fetch routes by path suffix: GET|POST …/stream attaches as SSE, POST …/frames submits statement frames (405 with Allow: POST otherwise), a WebSocket upgrade answers 426, anything else 404. upgrade serves /ws from any runtime's socket: it validates the handshake (400 {"detail":"statewire.v1 is required"} without the subprotocol, 400 {"detail":…} without a valid client query parameter, a thrown error for a request without Upgrade: websocket) and returns { protocol: "statewire.v1", attach }; attach(socket) after the handshake connects the socket, feeds text frames to the host (a binary frame gets fin: protocol-error then close 1008), and detaches on onclose. authorize runs on the socket's initial message with the upgrade headers merged in; a refusal is fin: unauthorized|forbidden then close 1008. After close, fetch and upgrade answer 410. Every StatewireHostInternal.SocketHostOptions field (protocol, key, authorize, heartbeatIntervalMs, …) passes through; an authorize refusal is a thrown Response.
| Name | Type | Default | Meaning |
|---|---|---|---|
transport | ResourceElement<Statewire> | required | The served transport |
clients? | StatewireHostInternal.ClientStore | — | Keeps client watermarks across restarts: a re-sent command answers expired instead of running twice |
StatewireNode.* | Shape |
|---|---|
Host | { fetch(request: Request): Promise<Response>; upgrade(request: Request): Promise<Response | Upgrade>; finish(fin: StatewireHostInternal.Fin): void; reconnect: StatewireHostInternal.SocketHost["reconnect"]; close(): Promise<void> }: close resolves once the client store holds the watermarks the host leaves behind |
Socket | StatewireHostInternal.Socket & { onmessage: ((event: { data: unknown }) => void) | null; onclose: (() => void) | null }: one accepted socket as the runtime exposes it; onmessage receives text frames as strings |
Upgrade | { protocol: "statewire.v1"; attach: (socket: Socket) => void } |
Options | StatewireHostInternal.SocketHostOptions & { transport; clients? } |
Serve it from node:http with @assistant-ui/pinned/node's toRequest/writeResponse, or from a pinned instance's fetch with /ws through upgradeResponse (apps/proxy/src/statewire.ts adapts the ws socket). Contract tests: packages/statewire/node/tests/host.test.ts.