# `:event-store` — bounded-tail event log (KMP) ## Что это Двухуровневое хранилище событий агента. Этот модуль — **короткий bounded tail** для live-SSE и недавнего replay. Полный audit log живёт в `:message-store-api` (никогда не эвиктится, source of truth). Три принципа: 1. **Tail управляет TTL сам.** Никаких `prune`/`cleanup` методов наружу — implementation решает, когда выкинуть старый event. Caller'ы не могут забыть cleanup. 2. **Catchup + live в одном Flow.** `events(after)` сначала отдаёт буферизованный диапазон, потом переключается на live tail — клиент не должен знать, где у него "разрыв". 3. **Read-only контракт для consumer'ов.** Запись через [MutableEventStore], чтение через [EventStore]. Compile-time гарантия что observer не сможет писать в store. ## Где используется - `:standalone` ChatAgent — append через `MutableEventStore` (заменяет текущий `agentEvents: MutableSharedFlow` + `persistAgentEvent`). - `:server` Routes.kt — `/events/all` SSE endpoint читает через `EventStore.events(after)`. - Будущий `:android-agent` core — same интерфейс для локального bounded tail без dedicated server connection. ## Архитектура ``` ┌─ :event-store (этот модуль) ────────────────────────┐ │ Bounded tail с auto-TTL: │ │ • append(event) ← producer │ │ • events(after): Flow ← consumer │ │ • earliestEventDate() для gap detection │ │ TTL/cap eviction — внутри impl │ └───────────────────────────────────────────────────┘ ▲ gap detected │ ┌─ :message-store-api (полный audit log) ───────────┐ │ MessageStore: query(after, before, limit) │ │ Никогда не эвиктится. Source of truth. │ └───────────────────────────────────────────────────┘ ``` **Reconnect pattern** (caller делает): ```kotlin val earliest = eventStore.earliestEventDate() if (client.lastSeen < earliest) { // gap: догоняем через :message-store-api val gap = messageStore.query(after = client.lastSeen, before = earliest) applyAll(gap) client.lastSeen = gap.last().createdAt } eventStore.events(after = client.lastSeen).collect { apply(it) } ``` ## API ### `EventStore` (read-only, для consumer'ов) ```kotlin interface EventStore : AutoCloseable { fun events(after: Instant?): Flow suspend fun earliestEventDate(): Instant // non-null: now() для пустого буфера override fun close() } ``` ### `MutableEventStore : EventStore` (для producer'ов) ```kotlin interface MutableEventStore : EventStore { suspend fun append(event: CommonEvent) } ``` **Append НЕ идемпотентен**: [CommonEvent] не имеет уникального id, retry даст дубликат. Для exactly-once — dedup через `:message-store-api` (там есть монотонный `id`). **Silently evicted**: implementation может выкинуть event сразу после append по TTL/cap. Producer не должен полагаться на то, что event дойдёт до клиента, если он вне retention window. ## Когда использовать какой интерфейс | Caller | Interface | |---|---| | ChatAgent (producer) | `MutableEventStore` | | Sub-agents (producer) | `MutableEventStore` | | Server SSE endpoint | `EventStore` | | Admin dashboard | `EventStore` | | Parent orchestrator | `EventStore` | | Тесты | `EventStore` (read-only) | ## Как добавить новый implementation 1. Создать класс с конструктором и lifecycle (`close()` обязан освободить ресурсы). 2. Реализовать минимум: append (с TTL eviction), events (Flow с catchup + live), earliestEventDate (non-null Instant, now() если буфер пуст). 3. Для persistent impl: SQL/ksqlite таблица с индексом по date, вставка = `INSERT OR IGNORE` для дедупликации на уровне БД (если в схеме будет id). ## Текущее состояние - ✅ Interface дизайн (`EventStore` + `MutableEventStore`) - ✅ KMP build (jvm + linuxX64 + mingwX64) - ⏳ Нет implementations (next: `InMemoryEventStore` для тестов) - ⏳ Не интегрирован в `:standalone`/`:server` ## Зависимости - `:proto` (api) — тип `CommonEvent` (3 AgentEvent + 9 Conversation.Event вариантов). - `kotlinx-coroutines-core` (api) — `Flow`. Никаких `kotlinx-serialization`, `kotlin-logging`, platform-specific зависимостей — этот модуль намеренно minimal.