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,6 +12,7 @@ import com.correx.core.events.types.SessionId
import com.correx.core.events.types.ValidationReportId
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
@@ -41,7 +42,7 @@ class DefaultApprovalRepositoryTest {
}
@Test
fun `after ApprovalRequestedEvent state contains the request`() {
fun `after ApprovalRequestedEvent state contains the request`(): Unit = runBlocking {
val sessionId = SessionId("session-requested")
val requestId = ApprovalRequestId("req-1")
@@ -66,7 +67,7 @@ class DefaultApprovalRepositoryTest {
}
@Test
fun `after ApprovalGrantCreatedEvent then ApprovalGrantExpiredEvent grants list is empty`() {
fun `after ApprovalGrantCreatedEvent then ApprovalGrantExpiredEvent grants list is empty`(): Unit = runBlocking {
val sessionId = SessionId("session-grant-expired")
val grantId = GrantId("grant-1")