feat(cas): content-addressed artifact store (steps 1–8)

New core:artifacts-store interface + infrastructure/artifacts-cas
implementation: segment files + SQLite index, Blake3 hashing,
group-commit fsync via flushBefore, recovery tail-scan, manual
compactor, and oldest-first disk-cap evictor.

Inference events now carry promptArtifactId / responseArtifactId;
orchestrators put bytes before emitting. SqliteEventStore wraps
its txn in artifactStore.flushBefore so segment data is fsynced
before the event commit, making the crash window non-corrupting
(TailScanner re-indexes orphan tail records on reopen).

Compactor and evictor are mutually exclusive via maintenanceMutex.
Step 9 (cloud sync) deferred to a later epic.

See docs/reviews/2026-05-18-cas-steps-1-8-review.md for the final
review.
This commit is contained in:
2026-05-18 12:22:38 +04:00
parent bbff73108e
commit 219e2c762e
60 changed files with 2042 additions and 165 deletions
@@ -12,7 +12,7 @@ import kotlinx.datetime.Clock
import java.util.*
class EventDispatcher(private val eventStore: EventStore) {
fun emit(
suspend fun emit(
payload: EventPayload,
sessionId: SessionId,
causationId: CausationId? = null,
@@ -1,5 +1,6 @@
package com.correx.core.events.events
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.ProviderId
import com.correx.core.events.types.SessionId
@@ -14,6 +15,7 @@ data class InferenceStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val promptArtifactId: ArtifactId,
) : EventPayload
@Serializable
@@ -24,6 +26,7 @@ data class InferenceCompletedEvent(
val providerId: ProviderId,
val tokensUsed: TokenUsage,
val latencyMs: Long,
val responseArtifactId: ArtifactId,
) : EventPayload
@Serializable
@@ -14,12 +14,12 @@ interface EventStore {
* - idempotency by eventId
* - causal consistency rules (if enforced here, otherwise in kernel)
*/
fun append(event: NewEvent): StoredEvent
suspend fun append(event: NewEvent): StoredEvent
/**
* Append multiple events atomically (same session preferred).
*/
fun appendAll(events: List<NewEvent>): List<StoredEvent>
suspend fun appendAll(events: List<NewEvent>): List<StoredEvent>
/**
* Read events for a session in strict sequence order.
@@ -41,4 +41,10 @@ interface EventStore {
* Does not replay historical events — use [readFrom] for catch-up before subscribing.
*/
fun subscribe(sessionId: SessionId): Flow<StoredEvent>
/**
* Iterate every event in the store across all sessions, in monotonic insertion order.
* Intended for batch jobs (e.g. CAS liveness scans). Implementations should stream lazily.
*/
fun allEvents(): Sequence<StoredEvent>
}