Conversations
Branches
Soft forks within a conversation — branch timelines, fork points, and branch-scoped queries.
Branches enable soft forks within a conversation. Forking at message N creates a branches row; subsequent messages on that line set branch_id. Main timeline messages keep branch_id = null.
Visual model
main: … → msg#10 → msg#11 → msg#12
branch B (fork at #10): msg#10' → msg#11' → msg#12'Both timelines share the same conversation_id. The fork point is recorded on the branch row.
Schema
branches
├── id (PK)
├── tenant_id
├── conversation_id
├── forked_from_message_id → messages.id
└── created_atMessages on a branch:
messages.branch_id → branches.idMain timeline: branch_id IS NULL.
Creating a branch
SDK:
const branch = await ctx.conversations.createBranch(
conversationId,
forkedFromMessageId,
);HTTP:
POST /v1/tenants/:tenant_id/conversations/:conversation_id/branches
{ "forkedFromMessageId": "..." }Emits conversation.branch.created on the stream.
Posting on a branch
await ctx.conversations.postMessage(conversationId, body, {
branchId: branch.id,
type: "message",
authorParticipantId: participantId,
});Querying
-- Main timeline only
SELECT * FROM messages
WHERE conversation_id = $1 AND branch_id IS NULL
ORDER BY created_at;
-- Specific branch
SELECT * FROM messages
WHERE conversation_id = $1 AND branch_id = $2
ORDER BY created_at;HTTP pagination accepts branchId query param — omit for main timeline.
GET .../messages?branchId=<uuid>&limit=50Use cases
| Scenario | Branch pattern |
|---|---|
| "Try again" from message N | Fork, continue alternate agent response |
| A/B comparison | Parallel branch timelines |
| Edit history | Prefer type: edit + relations for replacements; branches for divergent lines |
Agent processors
Processors typically read the main timeline (branch_id IS NULL) unless configured for branch-aware UIs. Branch traffic still delivers via conversation.message.posted with branchId in payload.