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:
@@ -19,6 +19,7 @@ dependencies {
|
||||
implementation project(':core:validation')
|
||||
implementation project(':core:risk')
|
||||
implementation project(':core:artifacts')
|
||||
implementation project(':core:artifacts-store')
|
||||
implementation project(':infrastructure')
|
||||
implementation project(':infrastructure:workflow')
|
||||
implementation project(':infrastructure:persistence')
|
||||
|
||||
@@ -31,7 +31,8 @@ import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
|
||||
fun main() {
|
||||
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore())
|
||||
val artifactStore = InfrastructureModule.createArtifactStore()
|
||||
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
|
||||
|
||||
val llamaProvider = InfrastructureModule.createLlamaCppProvider(
|
||||
modelId = System.getenv("CORREX_MODEL_ID") ?: "default",
|
||||
@@ -79,6 +80,7 @@ fun main() {
|
||||
repositories = repositories,
|
||||
engines = engines,
|
||||
retryCoordinator = DefaultRetryCoordinator(eventStore),
|
||||
artifactStore = artifactStore,
|
||||
)
|
||||
|
||||
val workflowRegistry = FileSystemWorkflowRegistry(InfrastructureModule.createWorkflowLoader())
|
||||
|
||||
@@ -11,7 +11,7 @@ private val log = LoggerFactory.getLogger(LoggingEventStore::class.java)
|
||||
|
||||
class LoggingEventStore(private val delegate: EventStore) : EventStore {
|
||||
|
||||
override fun append(event: NewEvent): StoredEvent =
|
||||
override suspend fun append(event: NewEvent): StoredEvent =
|
||||
runCatching { delegate.append(event) }
|
||||
.onSuccess { stored ->
|
||||
log.info(
|
||||
@@ -31,7 +31,7 @@ class LoggingEventStore(private val delegate: EventStore) : EventStore {
|
||||
}
|
||||
.getOrThrow()
|
||||
|
||||
override fun appendAll(events: List<NewEvent>): List<StoredEvent> =
|
||||
override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> =
|
||||
runCatching { delegate.appendAll(events) }
|
||||
.onSuccess { stored ->
|
||||
val breakdown = events.groupingBy { it.payload::class.simpleName }.eachCount()
|
||||
@@ -58,4 +58,6 @@ class LoggingEventStore(private val delegate: EventStore) : EventStore {
|
||||
override fun lastSequence(sessionId: SessionId): Long? = delegate.lastSequence(sessionId)
|
||||
|
||||
override fun subscribe(sessionId: SessionId): Flow<StoredEvent> = delegate.subscribe(sessionId)
|
||||
|
||||
override fun allEvents(): Sequence<StoredEvent> = delegate.allEvents()
|
||||
}
|
||||
|
||||
@@ -34,8 +34,12 @@ sealed class ServerMessage {
|
||||
data class InferenceStarted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
|
||||
|
||||
@Serializable
|
||||
data class InferenceCompleted(val sessionId: SessionId, val stageId: StageId, val outputSummary: String) :
|
||||
ServerMessage()
|
||||
data class InferenceCompleted(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val outputSummary: String,
|
||||
val responseText: String = "",
|
||||
) : ServerMessage()
|
||||
|
||||
@Serializable
|
||||
data class InferenceTimedOut(val sessionId: SessionId, val stageId: StageId, val elapsedMs: Long) :
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ private val log = LoggerFactory.getLogger(FileSystemWorkflowRegistry::class.java
|
||||
class FileSystemWorkflowRegistry(
|
||||
private val loader: WorkflowLoader,
|
||||
private val workflowsDir: Path = Path.of(
|
||||
System.getProperty("user.home"), ".config", "correx", "workflows"
|
||||
System.getProperty("user.home"), ".config", "correx", "workflows",
|
||||
),
|
||||
) : WorkflowRegistry {
|
||||
|
||||
@@ -32,8 +32,8 @@ class FileSystemWorkflowRegistry(
|
||||
.getOrNull()
|
||||
}
|
||||
.associateBy { graph ->
|
||||
graph.id.ifBlank { graph.let { it } .let { file -> file.start.value } }
|
||||
}
|
||||
graph.id.ifBlank { graph.start.value }
|
||||
}.also { log.info("cache init done, cache keys: {}", it.keys) }
|
||||
}
|
||||
|
||||
private fun logWarning(file: Path, ex: Throwable) {
|
||||
|
||||
@@ -48,7 +48,11 @@ class GlobalStreamHandler(private val module: ServerModule) {
|
||||
runCatching { ProtocolSerializer.decodeClientMessage(text) }
|
||||
.onSuccess { msg ->
|
||||
log.debug("recv: {}", msg::class.simpleName)
|
||||
handleClientMessage(session, msg)
|
||||
runCatching {
|
||||
handleClientMessage(session, msg)
|
||||
}.onFailure {
|
||||
log.error("handle failed", it)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
log.warn("decode error: {}", it.message)
|
||||
@@ -90,6 +94,7 @@ class GlobalStreamHandler(private val module: ServerModule) {
|
||||
|
||||
is ClientMessage.StartSession -> {
|
||||
val graph = module.workflowRegistry.find(msg.workflowId)
|
||||
log.info("find returned: {}", graph)
|
||||
if (graph == null) {
|
||||
val error = ServerMessage.ProtocolError("Unknown workflowId: ${msg.workflowId}")
|
||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||
|
||||
Reference in New Issue
Block a user