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
@@ -4,12 +4,14 @@ import com.correx.core.context.model.ContextPack
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.ProviderId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.utils.TypeId
import com.correx.core.inference.FinishReason
import com.correx.core.inference.GenerationConfig
import com.correx.core.inference.InferenceProvider
@@ -73,6 +75,9 @@ object InferenceFixtures {
)
}
private const val PLACEHOLDER_HEX_BYTES = 32
private val PLACEHOLDER_ARTIFACT_ID: ArtifactId = TypeId("00".repeat(PLACEHOLDER_HEX_BYTES))
fun inferenceCompleted(
requestId: InferenceRequestId,
sessionId: SessionId,
@@ -80,6 +85,7 @@ object InferenceFixtures {
providerId: ProviderId,
tokensUsed: TokenUsage,
latencyMs: Long,
responseArtifactId: ArtifactId = PLACEHOLDER_ARTIFACT_ID,
): InferenceCompletedEvent {
return InferenceCompletedEvent(
requestId = requestId,
@@ -88,6 +94,7 @@ object InferenceFixtures {
providerId = providerId,
tokensUsed = tokensUsed,
latencyMs = latencyMs,
responseArtifactId = responseArtifactId,
)
}
@@ -127,6 +134,7 @@ object InferenceFixtures {
providerId: ProviderId,
tokensUsed: TokenUsage,
latencyMs: Long,
responseArtifactId: ArtifactId = PLACEHOLDER_ARTIFACT_ID,
): NewEvent {
return NewEvent(
metadata = EventMetadata(
@@ -144,6 +152,7 @@ object InferenceFixtures {
providerId = providerId,
tokensUsed = tokensUsed,
latencyMs = latencyMs,
responseArtifactId = responseArtifactId,
),
)
}