The Chat Loop

Sessions and conversations

Every Ultron interaction is a Conversation backed by a stream of Messages. A Conversation tracks its project, persona, attached sandbox, and lifecycle state. Sessions resume from any device by reloading the Conversation id.

Updated today

Overview

At a glance
Table
conversations
Messages table
messages
Owner
One user per Conversation. Sharing creates a derived view.
Lifecycle
idle, running, paused, complete, failed
Resume key
Conversation id, opaque uuid

A Conversation is the durable thread that backs the chat UI. The user sees a session; the database sees a Conversation. The two terms map one to one. Every message the user sends, every tool call Claude makes, and every canvas the skill emits anchors to the same Conversation id.

Conversations belong to a project. A project is the user-facing folder used to keep work organized. The same user can open many Conversations in many projects; the chat handler scopes memory retrieval to the active Conversation and its sibling notes inside the same project.

Data model

The Conversation row and its supporting tables.

Conversation

supabase/migrations/conversations.sqlsql
1create table conversations (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid not null references auth.users(id) on delete cascade,
4 project_id uuid references projects(id) on delete set null,
5 title text,
6 persona text,
7 sandbox_id text,
8 state text not null default 'idle',
9 pause_point_id uuid,
10 last_message_at timestamptz,
11 created_at timestamptz not null default now()
12);

Message

supabase/migrations/messages.sqlsql
1create table messages (
2 id uuid primary key default gen_random_uuid(),
3 conversation_id uuid not null references conversations(id) on delete cascade,
4 role text not null check (role in ('user','assistant','tool','system')),
5 content jsonb not null,
6 tool_calls jsonb,
7 tool_call_id text,
8 turn_index int,
9 created_at timestamptz not null default now()
10);
Note
The content column is jsonb so it can hold blocks: text, code, canvas refs, attachment ids. This matches the message shape the model API expects on read back.

Session states

A Conversation moves through five states. The chat UI uses the state to decide whether to accept input, show a spinner, or surface an approval card.

StateMeaningUser can sendTriggered by
idleReady for the next user messageyesCreated or a turn just finished
runningA turn is executingnoChat handler accepted a message
pausedThe turn is waiting for approvalnowait_for_approval tool
completeConversation closed by the usernoUser archived the session
failedThe last turn errored outyesChat handler threw or the model returned an error
Tip
The state machine is the source of truth for UI affordances. If you want to know whether the input box should be active, read conversation.state.

Messages

Messages are appended in turn order. The chat handler reads the last N messages into context when starting a turn.

src/app/api/chat/v2/route.tsts
141// Build the conversation context the model sees on this turn
142const history = await loadMessages({
143 conversationId,
144 limit: WINDOW_SIZE, // recent messages kept verbatim
145 withToolCalls: true,
146})
147
148// Apply the four step compression cascade
149const compressed = await cascadeCompress(history, {
150 reserveTokens: TOKEN_BUDGET,
151})

Old turns do not stay verbatim forever. The compression cascade at src/lib/skills/memory.ts rewrites the oldest portion of the history into summaries before it ever reaches the model. See Conversation compression for the algorithm.

Resume

Resume is reloading the Conversation row, replaying its messages, reattaching its sandbox, and waking any paused turn.

src/app/api/chat/resume/route.tsts
1export async function POST(req: Request) {
2 const { pausePointId, approval } = await req.json()
3 const { supabase, userId } = await getAuth(req)
4
5 const pause = await supabase
6 .from('pause_points')
7 .select('*, conversations!inner(*)')
8 .eq('id', pausePointId)
9 .eq('user_id', userId)
10 .single()
11
12 if (!pause.data) return new Response('not found', { status: 404 })
13
14 await resumeTurn(pause.data, approval)
15 return new Response(null, { status: 204 })
16}
Note
Resuming a Conversation does not start a new turn. It unblocks the turn that paused. The model continues from the tool result, not from a fresh prompt.

Sharing

A Conversation can be shared as a read-only view at /s/[slug] without exposing the underlying user.

Sharing creates a row in shared_views with its own slug, optional gate (password, email, OTP, agreement), expiry, and view-limit counter. The public route at src/app/s/[slug]/page.tsx loads the shared view, enforces the gate, and renders the Conversation in read-only mode.

supabase/migrations/shared_views.sqlsql
1create table shared_views (
2 slug text primary key,
3 conversation_id uuid not null references conversations(id) on delete cascade,
4 gate_type text, -- null | 'password' | 'email' | 'otp' | 'agreement'
5 gate_secret text,
6 expires_at timestamptz,
7 view_count int not null default 0,
8 view_limit int,
9 created_at timestamptz not null default now()
10);

File map

src/app/api/chat
v2/route.tsmain handler
resume/route.tsresume from pause
src/app/api/conversations
route.tslist and create
[id]/route.tsload, update, archive
src/app/s
[slug]/page.tsxpublic shared view
src/lib/skills
memory.tscontext cascade
engine.tsskill execution loop
supabase/migrations
conversations.sql
messages.sql
shared_views.sql
pause_points.sql

Glossary

Conversation
The persisted thread that backs a chat session. One row in the conversations table.
Session
The UI presentation of a Conversation. The terms map one to one.
Project
A user-facing folder that groups conversations. Used to scope memory and shared notes.
Pause point
A row in pause_points written by the wait_for_approval tool. Resume unblocks the originating turn.
Shared view
A read-only public route for a Conversation, optionally gated by password, email, OTP, or agreement.
Turn index
An integer on each message that orders tool calls and assistant replies within a single turn.