Skip to content

feat: /conversations backing store — memory and Pebble backends #80

Description

@elevran

Scope

Implement the ConversationStore — the durable backing store for the /conversations endpoint. This is an internal package (internal/convstore) consumed by both cmd/conversationd (direct) and the proxy (via the convstore client or an embedded store in single-binary mode).

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 can span many response turns, each of which may have been stored in chainstore independently. The conversation store holds:

  • Conversation metadata: id, created_at, updated_at, title (optional), metadata (key-value)
  • 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.

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)

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
}

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
}

Backends

  • In-memory (convstore/memory): sync.RWMutex-protected maps; suitable for tests and the store: false-style use case. No TTL persistence.
  • 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.

Capacity management

  • 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

Acceptance criteria

  • internal/convstore package compiles with both in-memory and Pebble backends
  • ConversationStore interface covers Create / Get / Delete / AppendMessages / ListMessages
  • In-memory backend passes a shared test suite (convstore/testutil)
  • Pebble backend passes the same test suite
  • TTL eviction test: create conversations with short TTL, advance clock, verify reaped
  • Capacity eviction test: exceed MaxConversations, verify oldest evicted
  • MaxMessagesPerConversation cap returns correct error
  • make presubmit passes

Blocked by: #77

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions