fix(kernel,router): rehydrate by CAS content hash (F-021) + narration budget
rehydrate() now scans ArtifactContentStoredEvent (the durable slot->CAS-hash bridge) and fetches stored bytes by content hash, keying the cache by the logical slot name. The slot name is never passed to the hash-keyed CAS, so the odd-length-hex crash is gone and artifact content is restored on cold-start resume. RehydrateTest reworked to use a content-addressed fake store (slot name != content hash) so it actually exercises the bug. Replay (#8) is unaffected — rehydrate is live-resume-only. Also bump router narration maxTokens 1024 -> 4096: reasoning models were spending the whole budget thinking and emitting empty content (finishReason= length), so narrations never reached the TUI. Removes the stale TUI-refactor progress doc.
This commit is contained in:
@@ -4,7 +4,7 @@ import com.correx.core.approvals.DefaultApprovalRepository
|
||||
import com.correx.core.approvals.domain.DefaultApprovalEngine
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.artifacts.DefaultArtifactReducer
|
||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.types.ArtifactId
|
||||
@@ -44,15 +44,21 @@ import java.util.UUID
|
||||
class RehydrateTest {
|
||||
|
||||
private val sessionId: SessionId = TypeId("session-rehydrate")
|
||||
private val artifactId: ArtifactId = TypeId("artifact-001")
|
||||
|
||||
// The CAS is content-addressed: the stored key is a content hash, NOT the
|
||||
// logical slot name. F-021 — rehydrate must resolve slot.name -> contentHash
|
||||
// via ArtifactContentStoredEvent and fetch by hash, never pass the slot name
|
||||
// (which is non-hex and would crash a real CAS) to ArtifactStore.get.
|
||||
private val slotName: ArtifactId = TypeId("analysis")
|
||||
private val contentHash: ArtifactId = TypeId("a1b2c3d4")
|
||||
private val artifactContent = """{"status":"success"}"""
|
||||
|
||||
private val eventStore = InMemoryEventStore()
|
||||
|
||||
private val fakeArtifactStore: ArtifactStore = object : ArtifactStore {
|
||||
override suspend fun put(bytes: ByteArray): ArtifactId = artifactId
|
||||
override suspend fun put(bytes: ByteArray): ArtifactId = contentHash
|
||||
override suspend fun get(id: ArtifactId): ByteArray? =
|
||||
if (id == artifactId) artifactContent.toByteArray(Charsets.UTF_8) else null
|
||||
if (id == contentHash) artifactContent.toByteArray(Charsets.UTF_8) else null
|
||||
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
|
||||
}
|
||||
|
||||
@@ -100,7 +106,7 @@ class RehydrateTest {
|
||||
decisionJournalRepository = decisionJournalRepository,
|
||||
)
|
||||
|
||||
private suspend fun appendValidatedEvent() {
|
||||
private suspend fun appendContentStoredEvent() {
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
@@ -111,8 +117,9 @@ class RehydrateTest {
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
payload = ArtifactValidatedEvent(
|
||||
artifactId = artifactId,
|
||||
payload = ArtifactContentStoredEvent(
|
||||
artifactId = slotName,
|
||||
contentHash = contentHash,
|
||||
sessionId = sessionId,
|
||||
stageId = StageId("stage-1"),
|
||||
),
|
||||
@@ -121,20 +128,21 @@ class RehydrateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rehydrate populates artifactContentCache from ArtifactValidatedEvent`(): Unit = runBlocking {
|
||||
appendValidatedEvent()
|
||||
fun `rehydrate resolves slot name to content hash and populates cache`(): Unit = runBlocking {
|
||||
appendContentStoredEvent()
|
||||
|
||||
orchestrator.rehydrate(sessionId)
|
||||
|
||||
val cached = orchestrator.validatedArtifactContent(sessionId, artifactId)
|
||||
// Cache is keyed by the logical slot name; content fetched from CAS by hash.
|
||||
val cached = orchestrator.validatedArtifactContent(sessionId, slotName)
|
||||
assertEquals(artifactContent, cached)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rehydrate is a no-op when there are no ArtifactValidatedEvents`(): Unit = runBlocking {
|
||||
fun `rehydrate is a no-op when there are no ArtifactContentStoredEvents`(): Unit = runBlocking {
|
||||
orchestrator.rehydrate(sessionId)
|
||||
|
||||
val cached = orchestrator.validatedArtifactContent(sessionId, artifactId)
|
||||
val cached = orchestrator.validatedArtifactContent(sessionId, slotName)
|
||||
assertEquals(null, cached)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user