Scope
Implement the ConversationStore — the durable backing store for the /conversations endpoint. This is an internal package (internal/convstore) consumed by cmd/conversationd (direct) and the proxy (via the convstore client).
Design decisions
What a conversation is
A conversation is a named container for a sequence of messages. It is a higher-level concept than a response chain: a single conversation spans many response turns, each of which may have been stored in chainstore independently. All requests in a conversation are linear - there is no DAG/forest. A request can have either a conversation_id or a prev_response_id, not both. Requests in the chain are ordered by their created_at timestamp.
The conversation store holds:
- Conversation metadata:
id, created_at, updated_at, title (optional), metadata (up to 16 key-value string pairs)
- Message list: ordered append-only log of
Message values. Each message contains at minimum {role, content[]} (mirrors OpenAI Conversations API message objects).
ID allocation
Conversation IDs (conv_*) are minted by cmd/conversationd at POST /conversations time — not by the proxy. This mirrors how response IDs are assigned by the inference server. The proxy may pre-request an ID via POST /conversations before inference, or post-allocate after. A request stored in a conversation container
Storage key scheme (Pebble)
| Key prefix |
Value |
conv:meta:{id} |
Serialised ConversationMeta proto/JSON |
conv:msg:{id}:{seq:8} |
Serialised Message (seq is a zero-padded uint64 for lexicographic ordering) |
conv:lru:{bucket}:{id} |
Tombstone for LRU eviction index (mirrors chainstore bucket design) |
Question: per the specification, there is no conversation TTL. It should be an optional feature (default: disable) for automanagement of storage. When should the convesation's last acccessed by updated - on access to the conversation or its messages?
Interface
type ConversationStore interface {
Create(ctx context.Context, meta ConversationMeta) (ConversationMeta, error)
Get(ctx context.Context, id string) (ConversationMeta, error)
Delete(ctx context.Context, id string) error
AppendMessages(ctx context.Context, id string, messages []Message) (nextSeq uint64, err error)
ListMessages(ctx context.Context, id string, afterSeq uint64, limit int) ([]Message, error)
Ping(ctx context.Context) error
Close() error
}
Message store is delegated to the "chainstore" and supports its modes of appending (e.g., chunked, streamed, etc.).
Question: should the chainstore API be extended with a conversation identifier in order to NOT duplicate storage implementation?
Config mirrors chainstore.Config
type Config struct {
MaxConversations int64
MaxMessagesPerConversation int
MaxBytes int64
TTL time.Duration
TTLInterval time.Duration
EvictionInterval time.Duration
BucketDuration time.Duration
Clock Clock
Log *slog.Logger
Backend Backend // "memory" or pebble.Open()
Registerer prometheus.Registerer
}
See earlier comment on TTL.
Backends
- Pebble (
convstore/pebble): reuses the same Pebble DB opened by chainstore (passed in via Backend) or opens its own. Separate key prefix ensures no collision with chainstore keys. For testing, Pebble can be open in RAM only mode.
Capacity management
TTL and capacity are not part of the specification, but may wish to support them.
- LRU eviction when
MaxConversations or MaxBytes is exceeded (same bucket-based eviction as chainstore)
- Background TTL reaper (configurable interval)
MaxMessagesPerConversation cap: AppendMessages returns ErrConversationFull when exceeded
As reasonable default, 16b (64K) for MaxMessagesPerConversation cap.
Acceptance criteria
Blocked by: #77
Scope
Implement the
ConversationStore— the durable backing store for the/conversationsendpoint. This is an internal package (internal/convstore) consumed bycmd/conversationd(direct) and the proxy (via theconvstoreclient).Design decisions
What a conversation is
A conversation is a named container for a sequence of messages. It is a higher-level concept than a response chain: a single conversation spans many response turns, each of which may have been stored in
chainstoreindependently. All requests in a conversation are linear - there is no DAG/forest. A request can have either a conversation_id or a prev_response_id, not both. Requests in the chain are ordered by their created_at timestamp.The conversation store holds:
id,created_at,updated_at,title(optional),metadata(up to 16 key-value string pairs)Messagevalues. Each message contains at minimum{role, content[]}(mirrors OpenAI Conversations API message objects).ID allocation
Conversation IDs (
conv_*) are minted bycmd/conversationdatPOST /conversationstime — not by the proxy. This mirrors how response IDs are assigned by the inference server. The proxy may pre-request an ID viaPOST /conversationsbefore inference, or post-allocate after. A request stored in a conversation containerStorage key scheme (Pebble)
conv:meta:{id}ConversationMetaproto/JSONconv:msg:{id}:{seq:8}Message(seq is a zero-padded uint64 for lexicographic ordering)conv:lru:{bucket}:{id}chainstorebucket design)Question: per the specification, there is no conversation TTL. It should be an optional feature (default: disable) for automanagement of storage. When should the convesation's last acccessed by updated - on access to the conversation or its messages?
Interface
Message store is delegated to the "chainstore" and supports its modes of appending (e.g., chunked, streamed, etc.).
Question: should the chainstore API be extended with a conversation identifier in order to NOT duplicate storage implementation?
Config mirrors chainstore.Config
See earlier comment on TTL.
Backends
convstore/pebble): reuses the same Pebble DB opened bychainstore(passed in viaBackend) or opens its own. Separate key prefix ensures no collision with chainstore keys. For testing, Pebble can be open in RAM only mode.Capacity management
TTL and capacity are not part of the specification, but may wish to support them.
MaxConversationsorMaxBytesis exceeded (same bucket-based eviction as chainstore)MaxMessagesPerConversationcap:AppendMessagesreturnsErrConversationFullwhen exceededAs reasonable default, 16b (64K) for
MaxMessagesPerConversationcap.Acceptance criteria
internal/convstorepackage compiles with both in-memory and Pebble backendsConversationStoreinterface covers Create / Get / Delete / AppendMessages / ListMessagesconvstore/testutil) in disk and memory only modesMaxConversations, verify oldest evictedMaxMessagesPerConversationcap returns correct errormake presubmitpassesBlocked by: #77