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:
2026-06-09 10:14:04 +04:00
parent b407b47503
commit 89487db72a
5 changed files with 42 additions and 156 deletions
@@ -10,6 +10,7 @@ import com.correx.core.artifacts.ArtifactState
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RefinementIterationEvent
@@ -164,15 +165,17 @@ class DefaultSessionOrchestrator(
/**
* Rebuilds [artifactContentCache] from durable events after a server restart.
* Scans all events for [ArtifactValidatedEvent] and re-fetches the stored bytes
* from [artifactStore] for each one. Must be called before [resume] to ensure
* transition conditions that read artifact content work correctly.
* Scans all events for [ArtifactContentStoredEvent], which records the
* slot-name -> CAS content-hash mapping at write time, and re-fetches the
* stored bytes from [artifactStore] by **content hash** (not slot name).
* Must be called before [resume] so transition conditions and `needs`-injection
* that read artifact content work correctly after a cold start.
*/
suspend fun rehydrate(sessionId: SessionId) {
repositories.eventStore.readFrom(sessionId, fromSequence = 0L).forEach { event ->
val p = event.payload as? ArtifactValidatedEvent ?: return@forEach
val p = event.payload as? ArtifactContentStoredEvent ?: return@forEach
val key = "${sessionId.value}:${p.artifactId.value}"
artifactStore.get(p.artifactId)
artifactStore.get(p.contentHash)
?.toString(Charsets.UTF_8)
?.let { artifactContentCache[key] = it }
}