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:
@@ -16,6 +16,11 @@ fun activeSessionWidget(session: SessionSummary?): Paragraph {
|
||||
val lines = buildList<Line> {
|
||||
add(Line.from(Span.raw("stage: ${session.currentStage ?: "—"}")))
|
||||
session.lastOutput?.let { add(Line.from(Span.raw("last output: $it"))) }
|
||||
session.lastResponseText?.takeIf { it.isNotEmpty() }?.let { text ->
|
||||
add(Line.from(Span.raw("")))
|
||||
add(Line.from(Span.raw("response:")))
|
||||
text.lineSequence().forEach { add(Line.from(Span.raw(it))) }
|
||||
}
|
||||
}
|
||||
Text.from(lines)
|
||||
}
|
||||
|
||||
@@ -100,15 +100,7 @@ object SessionsReducer {
|
||||
)
|
||||
is ServerMessage.StageCompleted -> touchSession(sessions, msg.sessionId.value, null, clock)
|
||||
is ServerMessage.StageFailed -> touchSession(sessions, msg.sessionId.value, null, clock)
|
||||
is ServerMessage.InferenceCompleted -> sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) {
|
||||
s.copy(lastOutput = msg.outputSummary, lastEventAt = clock())
|
||||
} else {
|
||||
s
|
||||
}
|
||||
},
|
||||
)
|
||||
is ServerMessage.InferenceCompleted -> applyInferenceCompleted(sessions, msg, clock)
|
||||
is ServerMessage.ToolCompleted -> sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) {
|
||||
@@ -121,6 +113,24 @@ object SessionsReducer {
|
||||
else -> sessions
|
||||
}
|
||||
|
||||
private fun applyInferenceCompleted(
|
||||
sessions: SessionsState,
|
||||
msg: ServerMessage.InferenceCompleted,
|
||||
clock: () -> Long,
|
||||
): SessionsState = sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) {
|
||||
s.copy(
|
||||
lastOutput = msg.outputSummary,
|
||||
lastResponseText = msg.responseText.takeIf { it.isNotEmpty() },
|
||||
lastEventAt = clock(),
|
||||
)
|
||||
} else {
|
||||
s
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun touchSession(
|
||||
sessions: SessionsState,
|
||||
sessionId: String,
|
||||
|
||||
@@ -17,6 +17,7 @@ data class SessionSummary(
|
||||
val lastEventAt: Long,
|
||||
val currentStage: String?,
|
||||
val lastOutput: String?,
|
||||
val lastResponseText: String? = null,
|
||||
)
|
||||
|
||||
data class ApprovalInfo(
|
||||
|
||||
Reference in New Issue
Block a user