Skip to content

Architecture

How the glass box is built

One live push stream, watched by a dashboard that never trusts a decorative animation. The whole design turns on a single seam: components code against the ConversationSource interface, and the transport underneath swaps without touching a component.

01

The system map

Front to back. The browser holds a cold store and a hot store; the seam in the middle is the swap point; the transport below is either the in-browser engine, the FastAPI backend, or the provider direct.

System map // GLASSBOXbrowser -> seam -> transport
Browser / Next.js 16 App Router
The seam
Transport
Role
system map

Front to back

Hover or focus any node to read its role. Data flows down: components read the Query cache, the source writes it, one seq-stamped stream per conversation underneath. The seam in the middle is the piece that makes the transport swappable.

hover a node to inspect

02

Two stores, one truth

Server data and UI state are kept apart on purpose, so there is never a second copy to drift out of sync.

cold

TanStack Query

List pages and message history. The socket writer is the only thing that calls setQueryData; components read from the cache. One writer, one source of truth.

hot

Zustand

Ephemeral UI only: selection, scroll intent, composer draft, connection state, live-mode config, theme. It stores hasKey, never the key bytes. Server state is never mirrored here.

url

searchParams

Selection, filter and search live in the URL. Every view is deep-linkable and survives a reload, which is how the reasoning page jumps straight into a live conversation.

The write path

Incoming frames are coalesced in a buffer keyed by conversation and flushed to React state inside a single requestAnimationFrame, so a 200-frame burst is one paint and the UI commits at most 60 times a second regardless of inbound rate. Status transitions are latest-wins with a minimum dwell so rapid flips do not strobe.

03

Three data paths

The REST and WebSocket set is the contract the FastAPI backend implements. The live-AI path is separate: the browser talks to the provider directly. Each row calls out the single edge case it exists to prove.

Page loads and reconnect catch-up. Every read carries seq, so a WS frame that overlaps a REST row is deduped, never doubled.

GET/conversationslist summaries, {items, nextCursor}limit capped 1..100 (422 over); status validated against the enum (422 on junk); keyset cursor stays stable under live churn.
GET/conversations/{id}detail + newest-N messages404 if missing, 403 if another tenant, with a code comment that a 403 leaks existence, which stricter APIs avoid by returning 404.
GET/conversations/{id}/messagespaged history, {items, hasMore}after= returning more than limit means the client fell behind, so hasMore=true, never a silent truncation.
POST/conversations/{id}/messagesoperator sendrequires an Idempotency-Key (missing -> 422); returns 202 plus the echoed message with its seq; a double-submit with the same key posts once.
POST/conversations/{id}/takeovergrab the threadcompare-and-set; the operator who lost a two-operator race gets a 409.
GET/metricsstatus roll-up (StatusCounts)snake_case keys on the wire, matching the TypeScript type exactly.
04

The seq lifecycle

seq is a monotonic-per-conversation counter. Every emitted frame consumes the next one, so one number does three jobs at once.

01

Order

Every frame, whether message, message_update, ai_status or conversation_update, takes the next seq, so a subscriber sees a single totally ordered stream per conversation even when text and status arrive out of order.

02

Dedup

REST reads and WS frames overlap by design. The same seq delivered twice is dropped, so a backfill that races the live socket never doubles a message.

03

Gap detect

On reconnect the client subscribes after=lastSeq. If an incoming seq jumps past lastSeq + 1, frames were missed, so it backfills GET messages?after=lastSeq and then resumes.

05

Honest by construction

The live-AI path

When a viewer brings a key, the browser opens an SSE stream straight to Groq or OpenRouter. The key is read from localStorage, sent as a bearer token to the provider, and never sent to our server or logged. There is no proxy in the middle, so there is nothing of ours in the path to leak it.

Status bound to real events

thinking means actual work is in progress, replying means tokens are really streaming, waiting means genuinely idle. A thinking dwell past its threshold surfaces AI may be stuck so a hung agent gets a human. A dropped socket is shown as a reconnecting banner, because a dead socket otherwise looks like a quiet conversation. Provenance is surfaced instead of an invented confidence number.