feat: L3 memory retrieval on the router read path (record-only)

When a user sends input, embed it, query L3 across all sessions, dedup
against in-session turns, and record the retrieval as an event so replay
is deterministic (invariant #9). Hits land in RouterState; context
injection follows in a later slice.

- Add L3MemoryRetrievedEvent + L3RetrievedHit (registered in eventModule)
- RouterState.lastRetrievedMemory + reducer case; RouterTurn carries turnId
- RouterConfig.retrievalK (default 5)
- Harden L3 write path: runCatching + visible logging, cancellation
  re-thrown; event append stays ahead of the L3 write
- Warn prominently when the non-durable in_memory L3 backend is selected
This commit is contained in:
2026-05-30 14:36:30 +04:00
parent e8372e0643
commit e6239515bd
10 changed files with 340 additions and 37 deletions
@@ -19,3 +19,21 @@ enum class ChatTurnRole {
USER,
ROUTER,
}
@Serializable
@SerialName("L3MemoryRetrieved")
data class L3MemoryRetrievedEvent(
val sessionId: SessionId,
val queryTurnId: String,
val hits: List<L3RetrievedHit>,
val timestampMs: Long,
) : EventPayload
@Serializable
data class L3RetrievedHit(
val entryId: String,
val sourceSessionId: SessionId,
val sourceTurnId: String,
val text: String,
val score: Float,
)
@@ -10,6 +10,7 @@ import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.ChatSessionStartedEvent
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.L3MemoryRetrievedEvent
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceStartedEvent
@@ -73,6 +74,7 @@ val eventModule = SerializersModule {
subclass(RiskAssessedEvent::class)
subclass(ChatSessionStartedEvent::class)
subclass(ChatTurnEvent::class)
subclass(L3MemoryRetrievedEvent::class)
}
}