Skip to content

feat: /conversations backing store 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 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

  • internal/convstore package compiles with both in-memory and Pebble backends
  • ConversationStore interface covers Create / Get / Delete / AppendMessages / ListMessages
  • Pebble backend passes a shared test suite (convstore/testutil) in disk and memory only modes
  • 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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