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,30 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
jvm()
|
||||
macosX64()
|
||||
macosArm64()
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
linuxX64()
|
||||
linuxArm64()
|
||||
mingwX64()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.kotlinx.serialization.core)
|
||||
// для JsonElement в MessageContext.metadata
|
||||
api(libs.kotlinx.serialization.json)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package pw.binom.agentik.cursor
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Класс курсора
|
||||
*/
|
||||
@Serializable
|
||||
data class Cursor(
|
||||
/**
|
||||
* Время создания курсора. Unix Time в миллисекундах
|
||||
*/
|
||||
val createdAt: Long,
|
||||
|
||||
/**
|
||||
* Значение курсора
|
||||
*/
|
||||
val offset: ULong,
|
||||
) : Comparable<Cursor> {
|
||||
override fun compareTo(other: Cursor): Int {
|
||||
val byCreatedAt = createdAt.compareTo(other.createdAt)
|
||||
if (byCreatedAt != 0) return byCreatedAt
|
||||
return offset.compareTo(other.offset)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package pw.binom.agentik.cursor
|
||||
|
||||
/**
|
||||
* Держатель курсора: хранит текущую позицию и выдаёт следующие [Cursor]'ы.
|
||||
*
|
||||
* Один держатель — на одного агента, счётчик сквозной по всем сущностям
|
||||
* (conversation + message + lifecycle). Это даёт единый курсор на всё: и
|
||||
* глобальный (чат появился/умер/переименован), и per-chat — просто закладки в
|
||||
* одном потоке offset'ов.
|
||||
*
|
||||
* [Cursor.createdAt] — «эпоха» (момент обнуления счётчика): стабильна, пока
|
||||
* держатель персистентный, и меняется при сбросе/восстановлении БД. Первый
|
||||
* выданный offset — `1`; `0` означает «до первого события».
|
||||
*/
|
||||
interface CursorHolder {
|
||||
|
||||
/** Курсор, который будет выдан следующим [next] (next to assign). */
|
||||
fun current(): Cursor
|
||||
|
||||
/**
|
||||
* Выдать следующий курсор и сдвинуть позицию на `+1`. Первый вызов на
|
||||
* чистом счётчике даёт `offset = 1`.
|
||||
*/
|
||||
fun next(): Cursor
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package pw.binom.agentik.cursor
|
||||
|
||||
/**
|
||||
* Клиентская «закладка» курсора: хранит позицию, докуда локальное состояние
|
||||
* уже применено.
|
||||
*
|
||||
* В отличие от [CursorHolder] (серверный allocator, который *выдаёт* новые
|
||||
* offset'ы), этот holder их не генерирует — клиент только фиксирует уже
|
||||
* увиденные: `set(snapshot.cursor)` при полной синхронизации и
|
||||
* `set(event.offset)` по мере применения событий. Вызывать генерацию новых
|
||||
* offset'ов на клиенте нельзя — сфабрикованный курсор гарантированно даст
|
||||
* дырку.
|
||||
*
|
||||
* ## Семантика
|
||||
*
|
||||
* Инклюзивная: [current] — последний **применённый** курсор, т.е. «состояние
|
||||
* включает все события с `offset <= current()`». Возобновление подписки —
|
||||
* `events(after = current())` (эксклюзивно).
|
||||
*
|
||||
* `null` — ничего ещё не применено (первый запуск / сброшенный кэш): нужен
|
||||
* полный снапшот, после которого [set] фиксирует `snapshot.cursor`.
|
||||
*
|
||||
* ## Порядок записи
|
||||
*
|
||||
* [set] вызывается **после** того, как соответствующее состояние durable
|
||||
* применено. Курсор можно «отставать» от состояния (реплей идемпотентен), но
|
||||
* нельзя «убегать вперёд» — иначе на рестарте события между состоянием и
|
||||
* курсором потеряются.
|
||||
*
|
||||
* Реализация может персистить значение (durable-impl пишет сквозь), поэтому
|
||||
* [set] — `suspend`.
|
||||
*/
|
||||
interface MutableCursorHolder {
|
||||
|
||||
/** Последний применённый курсор, или `null`, если ещё ничего не применено. */
|
||||
fun current(): Cursor?
|
||||
|
||||
/**
|
||||
* Зафиксировать курсор. Вызывается после durable-применения состояния,
|
||||
* которому он соответствует; может писать в персистентное хранилище.
|
||||
*/
|
||||
suspend fun set(cursor: Cursor)
|
||||
}
|
||||
Reference in New Issue
Block a user