From 90ba7244d57674125c980ef58d6c353e2a383415 Mon Sep 17 00:00:00 2001 From: kami Date: Wed, 10 Jun 2026 12:35:07 +0400 Subject: [PATCH] fix(server): artifact viewer falls back to raw stage output when no content hash Artifacts with no ArtifactContentStored mapping (older sessions, or stages where the slot->hash link was lost) showed a blank pane. listArtifacts now falls back to the stage's last InferenceCompleted output so the operator can still see what the stage produced. Empty-string content is normalised to null so the viewer shows a clear '(no content stored)' instead of a blank pane. Verified: role_pipeline sessions already resolve content from CAS (index + segments intact, store rooted at ~/.config/correx/artifacts); the previously-blank sessions are healthcheck/degenerate runs whose inference output was genuinely empty. --- .../com/correx/apps/server/ws/StreamQueries.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/ws/StreamQueries.kt b/apps/server/src/main/kotlin/com/correx/apps/server/ws/StreamQueries.kt index 33064fb6..73d42e54 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/ws/StreamQueries.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/ws/StreamQueries.kt @@ -10,6 +10,7 @@ import com.correx.core.events.events.ArtifactContentStoredEvent import com.correx.core.events.events.ArtifactCreatedEvent import com.correx.core.events.events.ArtifactValidatedEvent import com.correx.core.events.events.ArtifactValidatingEvent +import com.correx.core.events.events.InferenceCompletedEvent import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.SessionId import kotlinx.coroutines.Dispatchers @@ -31,6 +32,11 @@ class StreamQueries(private val module: ServerModule) { suspend fun listArtifacts(sessionId: SessionId): ServerMessage.ArtifactList { val events = withContext(Dispatchers.IO) { module.eventStore.read(sessionId) } val accs = LinkedHashMap() + // Per-stage raw inference output (last response), used as a content fallback for artifacts + // that never recorded a slot->hash mapping (ArtifactContentStoredEvent) — e.g. older sessions + // or stages where the validated-artifact link was lost. Lets the operator still see what the + // stage actually produced instead of a blank pane. + val lastInferenceByStage = HashMap() for (event in events) { when (val p = event.payload) { is ArtifactCreatedEvent -> accs.getOrPut(p.artifactId.value) { ArtifactAcc() }.apply { @@ -40,19 +46,21 @@ class StreamQueries(private val module: ServerModule) { is ArtifactValidatingEvent -> accs[p.artifactId.value]?.let { it.phase = "VALIDATING" } is ArtifactValidatedEvent -> accs[p.artifactId.value]?.let { it.phase = "VALIDATED" } is ArtifactContentStoredEvent -> accs[p.artifactId.value]?.let { it.contentHash = p.contentHash } + is InferenceCompletedEvent -> lastInferenceByStage[p.stageId.value] = p.responseArtifactId else -> Unit } } val artifacts = accs.map { (id, acc) -> - val content = acc.contentHash?.let { hash -> - withContext(Dispatchers.IO) { module.artifactStore.get(hash) }?.toString(Charsets.UTF_8) + val hash = acc.contentHash ?: lastInferenceByStage[acc.stageId] + val content = hash?.let { + withContext(Dispatchers.IO) { module.artifactStore.get(it) }?.toString(Charsets.UTF_8) } ArtifactSummaryDto( artifactId = id, stageId = acc.stageId, schemaVersion = acc.schemaVersion, phase = acc.phase, - content = content, + content = content?.takeIf { it.isNotBlank() }, ) } return ServerMessage.ArtifactList(sessionId = sessionId, artifacts = artifacts)