Core Concepts
Threads
A thread is the top-level container for a conversation.
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
app_id | UUID | The Pingerchips app it belongs to |
status | enum | open | bot | pending | active | resolved |
title | string? | Optional display title |
bot_id | UUID? | Agent assigned to this thread |
assigned_agent | string? | Human agent handle (post-handoff) |
metadata | map | Arbitrary key-value pairs |
Thread state is stored in the Durable Objects ring and replicated to Postgres. Subscribe via:
chat:v1:app:{appKey}:thread:{threadId}Thread status flow
Messages
Real-time events carry the WAL message shape (camelCase):
| Field | Type | Description |
|---|---|---|
messageId | string | Client-minted UUID |
runId | string | The run that produced this message |
ownerClientId | string | client_id of the human who triggered the run |
role | string | user | assistant |
content | map | Codec-defined; { text } by default |
parentId | string? | Previous message in the branch |
forkOf | string? | Message this forks from (an edit) |
msgRegenerate | string? | Message this regenerates |
status | string | streaming | complete | cancelled | error |
createdAt | number | Unix ms |
The Chat JSON:API returns the
persisted Postgres shape, which additionally has thread_id, app_id,
user_id / bot_id, type, reactions, reply_count, edited_at,
deleted_at, inserted_at, updated_at.
Runs
A run is one LLM generation cycle.
During streaming, token chunks are buffered in a TokenBuffer GenServer and flushed every 40ms to the WAL as a stream:{runId} volatile slot. On run:end the final message is appended to messages and the volatile slot is dropped.
Conversation tree
Messages form a tree, not a list. parent_id links each message to the one it replies to. fork_of links a regenerated message to the original.
When a user regenerates, the server creates a new assistant message with fork_of pointing to the original. Both branches exist; the UI decides which to show.
GC compaction
The WAL grows with every write. Chat compacts on every run:end:
set_all is atomic — all preceding log entries for messages can be trimmed safely.
Load older / pagination
load_older walks backwards in pages of 50, from a before_message_id cursor.
Archive stubs are transparent to the client — it always receives a normal { messages, has_older } page.