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:
@@ -0,0 +1,31 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
}
|
||||
|
||||
// KMP-реализации :cursor-api в памяти — для тестов, dev-режима и
|
||||
// embedded-сценариев (Android core, CLI):
|
||||
// - `InMemoryCursorHolder` (серверный allocator): эпоха (`createdAt`)
|
||||
// генерируется при создании и не переживает пересоздание инстанса;
|
||||
// - `InMemoryMutableCursorHolder` (клиентская закладка).
|
||||
//
|
||||
// Зависимости: только `:cursor-api`. Никакого I/O — pure in-memory.
|
||||
//
|
||||
// Цели сборки — jvm() + linuxX64() + mingwX64() (как у `:outbox-inmemory`).
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
|
||||
jvm()
|
||||
linuxX64()
|
||||
mingwX64()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(project(":cursor-api"))
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package pw.binom.agentik.cursor.inmemory
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
import pw.binom.agentik.cursor.CursorHolder
|
||||
import kotlin.time.Clock
|
||||
|
||||
/**
|
||||
* In-memory [CursorHolder] — для тестов, dev-режима и ephemeral runtime.
|
||||
*
|
||||
* `createdAt` генерируется при создании и **не переживает** пересоздание
|
||||
* инстанса: новый holder → новая эпоха → клиент с прежним курсором получит
|
||||
* `OutboxGapException` и сделает resync. Для production-агента нужен
|
||||
* персистентный holder (см. `:cursor-ksqlite`).
|
||||
*/
|
||||
class InMemoryCursorHolder(
|
||||
private val createdAt: Long = Clock.System.now().toEpochMilliseconds(),
|
||||
initialOffset: ULong = 1u,
|
||||
) : CursorHolder {
|
||||
|
||||
private var counter: ULong = initialOffset
|
||||
|
||||
override fun current(): Cursor = Cursor(createdAt, counter)
|
||||
|
||||
override fun next(): Cursor {
|
||||
val assigned = current()
|
||||
counter += 1u
|
||||
return assigned
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package pw.binom.agentik.cursor.inmemory
|
||||
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
import pw.binom.agentik.cursor.MutableCursorHolder
|
||||
|
||||
/**
|
||||
* In-memory [MutableCursorHolder].
|
||||
*
|
||||
* Курсор живёт только в RAM: после пересоздания инстанса (рестарт процесса)
|
||||
* он теряется, и клиент снова пойдёт за снапшотом. Для переживания рестарта
|
||||
* нужен durable-impl (файл / DataStore / SQLite) со стороны хоста — этот класс
|
||||
* лишь даёт готовую реализацию для dev/test/ephemeral-сценариев.
|
||||
*
|
||||
* @param initial стартовое значение; обычно результат загрузки из хранилища.
|
||||
*/
|
||||
class InMemoryMutableCursorHolder(
|
||||
initial: Cursor? = null,
|
||||
) : MutableCursorHolder {
|
||||
|
||||
private var value: Cursor? = initial
|
||||
|
||||
override fun current(): Cursor? = value
|
||||
|
||||
override suspend fun set(cursor: Cursor) {
|
||||
value = cursor
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package pw.binom.agentik.cursor.inmemory
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import pw.binom.agentik.cursor.Cursor
|
||||
|
||||
class InMemoryMutableCursorHolderTest {
|
||||
|
||||
@Test
|
||||
fun startsEmpty() {
|
||||
assertNull(InMemoryMutableCursorHolder().current())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startsFromInitial() {
|
||||
val c = Cursor(createdAt = 1_700_000_000_000L, offset = 42u)
|
||||
assertEquals(c, InMemoryMutableCursorHolder(initial = c).current())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setReplacesValue() = runTest {
|
||||
val holder = InMemoryMutableCursorHolder()
|
||||
val first = Cursor(createdAt = 1_700_000_000_000L, offset = 1u)
|
||||
val second = Cursor(createdAt = 1_700_000_000_000L, offset = 2u)
|
||||
|
||||
holder.set(first)
|
||||
assertEquals(first, holder.current())
|
||||
|
||||
holder.set(second)
|
||||
assertEquals(second, holder.current())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user