Добавляет Cursor/OffsetSequencer в :outbox-api и интегрирует PersistentOffsetSequencer через :outbox-ksqlite.
Введение монотонного offset'а как персистентного состояния агента:
offset'ы переживают рестарт standalone-агента, клиент продолжает
синхронизацию инкрементально, без полной re-sync с нуля.
outbox-api:
- Cursor (offset: Long) — курсор в журнале событий агента.
- OffsetSequencer — интерфейс резервирования уникального offset.
- CursorStore — персистентное хранилище текущего offset'а.
- PersistentOffsetSequencer — декоратор над любым OutboxStore,
обновляет CursorStore на каждом append (atomic transaction).
- OutboxGapException — клиент запросил after < earliestCursor() →
сервер не может удовлетворить, клиент обязан делать full resync.
- DurableEvent переименован из Event.kt → DurableEvent.kt (Event.kt
был общим sealed-типом, теперь это термин из спеки).
- MutableOutboxStore и OutboxStore теперь читают offset через
CursorStore вместо in-memory counter'а.
outbox-inmemory:
- InMemoryOffsetSequencer — для тестов и dev-режима.
- InMemoryOutboxStore теперь принимает OffsetSequencer в конструкторе.
outbox-ksqlite (новый модуль):
- KsqliteCursorStore — таблица outbox_cursor (agent_id TEXT PK,
offset INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL).
- KsqliteCursorStoreTest — 4 теста (set/get, monotonic, concurrent).
proto + server:
- Snapshot.proto — server-state snapshot endpoint для клиентов,
которым нужна полная материализация (использование TBD).
- Routes.kt + SnapshotRouteTest — endpoint /agentik/snapshot (GET).
journal-ksqlite:
- KsqliteJournalStore.listFlow/append — без изменений по API,
нотации минимальные (codecs).
standalone:
- DurableLog (бывший ChatAgent-orchestration) — атомарный commit
события в OutboxStore + PersistentOffsetSequencer + materialization
(через Reducer) одной транзакцией.
- SqliteStores — добавляет KsqliteCursorStore в bundle, единая
shared-connection для всех ksqlite-сторов standalone-агента.
- ChatAgent / ConversationLoop / ConversationEvents / ReflectionScheduler /
ToolDispatcher — переход на новые абстракции.
- standalone/build.gradle.kts — implementation(project(':outbox-ksqlite'))
включено (раньше было закомментировано — модуль только создавался).
client:
- AgentikAgent / AgentClient / HttpEventStore / HttpJournalStore /
ReconnectingOutbox — используют Cursor через transport API.
- client/README.md — синхронизирован с новым поведением (468 строк
diff — это в основном оформление и примеры).
kotlinx-io: 0.8.0 → 0.9.1 в libs.versions.toml (см. sync-core tests).
SYNC-SYSTEM.md (в корне) — спецификация, на которую ссылается и
:sync-core (эта сессия), и эта Cursor-абстракция в outbox-api.
Тесты: standalone 132, journal-ksqlite 25, outbox-inmemory 20,
outbox-ksqlite 4, client 10, sync-core 74 — все зелёные на jvm;
sync-core linuxX64 74 тоже зелёный.
sync2/ (заброшенный stub с одним build.gradle.kts) удалён.
This commit is contained in:
+20
-12
@@ -15,21 +15,12 @@ import kotlin.time.Instant
|
||||
* embedded/CLI сценариев достаточно; для hot-path на сервере используйте
|
||||
* [pw.binom.agentik.journal.ksqlite.KsqliteJournalStore].
|
||||
*
|
||||
* **Контракт `list`**: возвращает подмножество с
|
||||
* `conversationId == conversationId && createdAt > after`, отсортированное
|
||||
* по `createdAt ASC`. `offset/limit` — paging поверх отфильтрованного списка.
|
||||
* **Контракт `list`**: legacy — `createdAt > after`, `ORDER BY createdAt ASC`
|
||||
* (+`offset/limit`); seq-версия — `afterSeq < seq <= upToSeq`,
|
||||
* `ORDER BY seq ASC` (keyset).
|
||||
*
|
||||
* **Очистка**: [clear] сбрасывает кэш (например, когда диалог удалён
|
||||
* на сервере). [close] — no-op.
|
||||
*
|
||||
* Типичный кэш-паттерн в клиенте:
|
||||
* ```
|
||||
* val local = InMemoryJournalStore()
|
||||
* val remote = HttpJournalStore(httpClient, baseUrl)
|
||||
* // backfill + кэширование:
|
||||
* remote.listFlow(convId, Instant.DISTANT_PAST).collect { local.append(it) }
|
||||
* // после этого `local.list(convId, after, offset, limit)` отдаёт из кэша.
|
||||
* ```
|
||||
*/
|
||||
class InMemoryJournalStore : MutableJournalStore {
|
||||
|
||||
@@ -54,6 +45,19 @@ class InMemoryJournalStore : MutableJournalStore {
|
||||
.toList()
|
||||
}
|
||||
|
||||
override suspend fun list(
|
||||
conversationId: String,
|
||||
afterSeq: Long,
|
||||
upToSeq: Long,
|
||||
limit: Int,
|
||||
): List<MessageRecord> = mutex.withLock {
|
||||
records.asSequence()
|
||||
.filter { it.conversationId == conversationId && it.seq > afterSeq && it.seq <= upToSeq }
|
||||
.sortedWith(compareBy({ it.seq }, { it.createdAt }, { it.id }))
|
||||
.take(limit)
|
||||
.toList()
|
||||
}
|
||||
|
||||
/** Удалить все записи диалога (каскад из ChatAgent.deleteConversation). */
|
||||
override suspend fun clear(conversationId: String): Unit = mutex.withLock {
|
||||
records.removeAll { it.conversationId == conversationId }
|
||||
@@ -67,6 +71,10 @@ class InMemoryJournalStore : MutableJournalStore {
|
||||
records.count { it.conversationId == conversationId && it.createdAt > after }.toLong()
|
||||
}
|
||||
|
||||
override suspend fun count(conversationId: String, afterSeq: Long): Long = mutex.withLock {
|
||||
records.count { it.conversationId == conversationId && it.seq > afterSeq }.toLong()
|
||||
}
|
||||
|
||||
/** Сколько записей сейчас в кэше. Для тестов/диагностики. */
|
||||
suspend fun size(): Int = mutex.withLock { records.size }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user