Kanzo UI
The design of Kanzo UI

How a failure leaves the library

The thrown value whole, by one path; the codes kanzo-ui names itself; and the waits it bounds. The argument across fossil, kanzo-ui and their hosts is fossil's page, not this one.

The rule spans three repositories, and its argument lives once, in fossil's docs/content/docs/design/failure.mdx (Every wait ends, every failure is seen). This page holds the two things only this library can: what its signatures promise a host, and the list of codes it names itself.

The thrown value, whole, by one path

Every surface that hands a host a failure hands it the unknown that was thrown: GraphRoot's onFailure, MosaicProvider's onFailure, AssistProvider's onFailure, useBlocker's onFailure, useSession()'s error beside status: "failed", engine()'s rejection. A FossilError arrives as a FossilError. The library never reduces a failure to its message, never depends on fossil's types to pass one, and never turns one into another state — an empty chart, "anonymous", a spinner.

What would reverse it: a second consumer that wants failures in a vocabulary of its own. Then the codes below become data on one kanzo-ui error type rather than a list a host must know.

Held by packages/ui/src/failures-reach-the-host.test.ts, "bites on onFailure, onError or error typed string" and "bites on a class extending MosaicClient with no queryError".

The codes this library names

Each is an Error with code and data, in the area/kind grammar fossil's and a host's codes use, so one host registry keys all three. One class per package, named for it.

CodeClassWhendata
engine/unavailableEngineErrorDuckDB-WASM would not bootafter when the 60 s deadline fired
graph/no-webglGraphErrorno WebGL, a renderer that would not start, or no device within 10 safter when the deadline fired
graph/context-lostGraphErrorthe browser took the WebGL context back—
graph/no-positionsGraphErrora device that came up holds no positions for the points it was given: a frame's read-back failed—
graph/nothing-to-drawGraphErroran attached corpus with no vertex—
graph/unfilterableGraphErrora crossfilter clause naming columns no vertex table has—
ai/silentAiError, from @kanzo-tech/llma model behind createGateway sent nothing for 30 s — no headers, or no chunk since the lastafter
session/*, idp/*, callback/*, token/*, organization/*, claims/*AuthErrorsee the auth layerafter, status where named

What would reverse it: a code a host never branches on. Then it folds into its neighbour; the list is short because every row is a different thing to show.

The waits it bounds

The figures are fossil's table, and move there with the measurement beside them. Two are this library's own: DuckDB's boot, 60 s, and cosmos.gl's device, 10 s — the second is not in fossil's table yet. Work inside the process — a query, a layout — has a signal and no deadline, and so does fossil's open, which the host awaits before it names the catalog: the network waits inside it are fossil's to bound.

What would reverse it: a device that legitimately takes longer than 10 s to come up, measured.

Held by packages/mosaic/src/engine-boot.test.ts, "rejects with data.after when the worker never answers within the 60 s deadline".

A model's silence is bounded once, by the door that makes the request

createGateway owns the request to the model, so it owns the wait: a streaming request's headers are due within 30 s and each chunk of its body within 30 s of the last, and past that the request is aborted and the caller sees ai/silent. Nothing above it bounds the same wait again — not Assist, not Chat, not a host. A second bound on one wait is two parties racing to name one failure, and the one that wins is whichever timer was armed first.

The figure is fossil's for a stream's next chunk. Three things follow from where it sits:

  • Silence is the network's, not the model's thinking. A model reasoning inside <think> is still sending chunks; only a stream that sends nothing is cut. A bound above the SDK would count only the text it yields and cut a model mid-thought.
  • A request that does not stream is not bounded. Its headers wait on the whole answer, which is work on the far side: there is no silence to tell apart from it. The caller's signal is its end.
  • The library's own asks are made once. Assist passes maxRetries: 0, so a gateway's 504 is the failure it shows, with the gateway's code, on the first attempt. The SDK's default is two retries with backoff: a gateway that gives up after its own 30 s was asked three times and surfaced some 96 s later, as a RetryError with the 504 inside it.

A model that does not come from createGateway — a mock, a host's own provider — has no bound here. That is the price of one layer, and it is paid by whoever skips the door.

What would reverse it: a host that must bound a model stream differently from every other — a longer idle for a model measured to pause past 30 s mid-answer. Then the figure becomes a GatewaySettings field, still on the one door. A silence the fetch cannot see — a stream that keeps sending bytes and never a token — would put a second bound above the SDK, and it has not been seen.

Held by packages/llm/src/gateway.test.ts, "ends a stream whose headers do not come within 30 s as ai/silent, and not a millisecond before", "ends a stream that stops sending mid-way, 30 s after its last chunk", "does not cut a request that does not stream, whose headers wait on the whole answer"; packages/ai/src/assist.test.tsx, "asks once: a gateway's 504 is the failure, not a retry after it".

On this page