Skip to content

Storage

Where the chat of record lives — the durable conversation the UI renders and the agent reads back.

Postgres, behind agent/chat

The chat is transactional, relational data: Chat → Message, tied to Agent / Team. It needs strong consistency and clean pagination for the UI → Postgres is the right home.

prisma
model Chat {
  id        String    @id            // chat-{uuid}
  agentId   String
  channel   String                   // widget | telegram | api …
  messages  Message[]
  createdAt DateTime  @default(now())
  @@index([agentId, createdAt])
}

model Message {
  id        String   @id             // msg-{uuid}
  chatId    String
  role      String                   // user | assistant | tool | system
  content   Json                     // text + tool refs + attachments
  tokens    Int?
  createdAt DateTime @default(now())
  chat      Chat     @relation(fields: [chatId], references: [id])
  @@index([chatId, createdAt])
}

The agent/chat slice owns this behind a gateway, so the physical store is swappable without touching callers.

Reading history: pages, not whole threads

All three history reads are keyset-paginated, and there is no way to ask for everything:

GET /agents/:id/chat/messages?limit=&cursor=
GET /agents/:id/chat/threads?limit=&cursor=
GET /agents/:id/chat/threads/:threadId/messages?limit=&cursor=
  • limit defaults to 50 and is capped at 200.
  • The answer is { items, nextCursor, hasMore } with items ordered oldest → newest. hasMore is its own field on purpose: inferring it from items.length === limit is wrong exactly once per thread — on a last page that happens to be full.
  • cursor is opaque and walks backwards, into older rows. A cursor this api did not issue is rejected, not quietly ignored.

This is not a nicety. An agent with autostart writes to its thread around the clock, so a thread grows without anyone using the product.

Starting over — a line in the thread, not a second thread

A conversation had nowhere to end: something was settled, a week later somebody arrived with something else, and the model dragged the whole first subject into the second. "Start over" draws a boundary row in the same thread. From it the model is handed nothing that came before, while the person keeps every earlier message and scrolls to it.

There is still exactly one thread per (agent, channel, person) — the key says so — and no new kind of row: the boundary is the same marker compaction writes when it replaces older turns with a recap, placed deliberately instead of waiting for the thread to grow. Pressing the button twice does not stack two dividers.

What a restart does not touch is the agent's memory. Notes it wrote about the person are not in the thread, so after starting over it still knows them. That looks like a failure and is not, which is why the screen says so rather than leaving the person to discover it.

What is not stored

  • A turn that fails persists nothing. A turn the user stops is different: the text already delivered is kept and marked interrupted, so the thread after a reload matches what was on screen. See What ends a turn.

Volume — Postgres handles it

"Significant volume" is the normal case for a Message table; the levers:

  • Partition Message (by time, or by agentId/teamId) so hot ranges stay small.
  • Index for the real queries (chatId, createdAt); avoid wide scans.
  • Archive cold conversations to object storage (the row keeps a pointer); the live table stays lean.
  • The raw trace/prompt blobs do NOT live here — those go to Debug & tracing. Keeping telemetry out is what keeps this table fast.

Start shared → split when it earns it

  1. Now: chats live in the main Postgres (one DB, simplest).
  2. At scale: move the agent/chat data to a dedicated Postgres instance when chat write-IO starts competing with transactional load. Because it's behind the gateway, this is a config change, not a rewrite — exactly the "in the api first, separate database later" path.

See also

  • Debug & tracing — why prompts/traces stay out of this table.
  • Memory — the search index derived from these messages.
  • DB schemaChat / Message in the wider model.