refactor: outbox становится live-only стримом, cursor вынесен в :cursor-api
Семантика outbox'а — теперь чистый live-канал:
* OutboxStore.conversationEvents(id) — больше не принимает after-cursor.
Catchup (replay) делает клиент: journal.listFlow(afterSeq) + подписка
на live. Outbox ответственен только за уведомления «что-то произошло».
* OutboxStore.oldestCursor()/currentCursor()/OutboxGapException — удалены.
Эпоха и offset живут ТОЛЬКО в CursorHolder/CursorStore, переживают
рестарт и инкремент для каждого commit.
* DurableEvent: commit принимает блок { cursor -> MessageRecord }
(cursor выдаёт CursorStore; клиент не вычисляет offset сам).
* MessageRecord больше не несёт cursor — это не его ответственность.
Новые модули:
* :cursor-api — Cursor(epoch, offset) + CursorHolder / MutableCursorHolder
* :cursor-ksqlite — KsqliteCursorHolder (персистентный)
* :cursor-inmemory — для тестов
* :client-sync — LocalSyncAgent (мини-агент поверх :client для десктопа)
Удалены:
* :sync-core — старая референсная реализация, заменена
cursor-разделением и :client.
* outbox-ksqlite — CursorStore/Schema уехали в :cursor-ksqlite.
* OffsetSequencer / PersistentOffsetSequencer / InMemoryOffsetSequencer.
standalone:
* ChatAgent/ConversationLoop/DurableLog/ToolDispatcher/ReflectionScheduler/
ConversationEvents — подписка через push-паттерн (collect событий).
* A2aBridge — currentCursor() и conversationEvents(after=) убраны.
* SqliteStores — cursor_offset удалён из schema v4; seedNextFromJournal
читает MAX(created_at).
* Main.kt — outboxSequencer → outboxCursorHolder; user→agent (:server)
transport удалён; debug-routes удалены; A2A остался.
* Тесты ChatAgentTest/PersistenceTest переписаны на push-паттерн
(subscribe-before-act, snapshot∪live = итоговое состояние). 25/25 + 19/19 ✅
server / client:
* Routes эпоху читают из CursorHolder; снимки несут Cursor? для catchup.
* AgentikAgent и HttpEventStore — те же подписки, без after-параметра.
* ReconnectingOutbox / ReconnectingOutboxTest — без изменений API.
* JournalStore API расширен count(after=Instant?) для unread-badge.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
package pw.binom
|
||||
|
||||
/**
|
||||
* Сигнал о том, что запрошенный курсор уже скомпактирован и запрошенные события недоступны.
|
||||
*/
|
||||
class CursorExpiredException : RuntimeException()
|
||||
@@ -2,4 +2,7 @@ package pw.binom
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
/**
|
||||
* Событие журнала: полезная нагрузка [data] и её позиция [seq] в общем потоке.
|
||||
*/
|
||||
data class Event<EVENT>(val data: EVENT, val seq: Cursor)
|
||||
@@ -3,6 +3,13 @@ package pw.binom
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
/**
|
||||
* Результат чтения журнала от курсора: либо поток событий, либо признак того, что запрошенный
|
||||
* курсор уже скомпактирован ([isExpired]).
|
||||
*
|
||||
* Реализован как value-класс без боксинга потока; вместо исключения «протухший» курсор
|
||||
* возвращается явно, а [getOrException] бросает [CursorExpiredException].
|
||||
*/
|
||||
@JvmInline
|
||||
value class EventSeqResult<EVENT> private constructor(val raw: Any) {
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@ package pw.binom
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
/**
|
||||
* Журнал событий, читаемый по курсору (только чтение).
|
||||
*
|
||||
* [minCursor]/[maxCursor] — границы хранимого диапазона (null для пустого журнала). [get] отдаёт
|
||||
* все события начиная с [cursor] включительно, либо [EventSeqResult.expired], если [cursor]
|
||||
* старше минимального хранимого. Запись — в [MutableEventStore].
|
||||
*/
|
||||
interface EventStore<EVENT> : AutoCloseable {
|
||||
|
||||
val minCursor: Cursor?
|
||||
|
||||
@@ -40,7 +40,7 @@ import pw.binom.db.ksqlite.transaction
|
||||
* выбрасывается посреди потока.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class KSqliteMutableEventStore<EVENT>(
|
||||
class KSqliteMutableEventStore<EVENT> private constructor(
|
||||
private val connection: SQLiteConnection,
|
||||
private val ownsConnection: Boolean,
|
||||
private val tableName: String,
|
||||
|
||||
@@ -2,6 +2,10 @@ package pw.binom
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
/**
|
||||
* Изменяемый журнал событий: к чтению из [EventStore] добавляет дозапись ([push]), полную очистку
|
||||
* ([clear]) и компакцию ([dropAllBefore], удаляет всё до [cursor] включительно).
|
||||
*/
|
||||
interface MutableEventStore<EVENT> : EventStore<EVENT> {
|
||||
fun push(event: Event<EVENT>) = push(event.data, event.seq)
|
||||
fun push(event: EVENT, cursor: Cursor)
|
||||
|
||||
@@ -2,6 +2,10 @@ package pw.binom
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
/**
|
||||
* Материализует [STATE] из событий: [apply] применяет событие с позицией [cursor] к состоянию.
|
||||
* Как именно — решает сам стор, отдельный редьюсер не требуется.
|
||||
*/
|
||||
interface MutableStateStore<STATE, EVENT> : StateStore<STATE> {
|
||||
fun apply(event: EVENT, cursor: Cursor)
|
||||
fun apply(event: Event<EVENT>) = apply(event.data, event.seq)
|
||||
|
||||
@@ -2,11 +2,12 @@ package pw.binom
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
interface MutableStorage<STATE, EVENT>: AutoCloseable {
|
||||
/**
|
||||
* Хранилище, совмещающее материализованное состояние ([StateStore]) и журнал событий
|
||||
* ([EventStore]) в одном месте — локальная БД клиента, из которой рендерится UI.
|
||||
* [push] дозаписывает событие в журнал.
|
||||
*/
|
||||
interface MutableStorage<STATE, EVENT>: StateStore<STATE>,EventStore<EVENT> {
|
||||
fun push(event: EVENT, cursor: Cursor)
|
||||
|
||||
/**
|
||||
* Возвращает состояние на момент [cursor]
|
||||
*/
|
||||
fun getState(cursor: Cursor): STATE
|
||||
fun clear()
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package pw.binom
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
interface StateStore<STATE> {
|
||||
|
||||
|
||||
/**
|
||||
* Материализованное из журнала состояние, читаемое по курсору (только чтение).
|
||||
* Наполнение — в [MutableStateStore].
|
||||
*/
|
||||
interface StateStore<STATE> : AutoCloseable {
|
||||
/**
|
||||
* Возвращает состояние на момент [cursor]
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,9 @@ private const val EPOCH = 1_700_000_000_000L
|
||||
@Serializable
|
||||
private data class TestEvent(val value: Int)
|
||||
|
||||
/**
|
||||
* Тесты ksqlite-реализации журнала событий [KSqliteMutableEventStore] на in-memory SQLite.
|
||||
*/
|
||||
class KSqliteMutableEventStoreTest {
|
||||
|
||||
private fun cursor(offset: ULong, createdAt: Long = EPOCH) = Cursor(createdAt = createdAt, offset = offset)
|
||||
|
||||
Reference in New Issue
Block a user