feat: rehydrate L3 metadata from event log on startup

TurboVec persists vectors via the sidecar but kept entry metadata
(sessionId/turnId/text) only in an in-memory map, so restarting with
backend=turbovec silently dropped every query hit (the id→metadata
lookup missed). Rebuild that map from the ChatTurnEvent log at startup.

- RehydratableL3MemoryStore: capability interface for stores whose
  vectors persist independently of the JVM. TurboVec implements it;
  InMemory deliberately does not (its vectors are volatile too).
- L3MetadataRehydrator replays ChatTurnEvents into the metadata map.
  No embedder: the query path never reads entry.vector, so vectors
  stay in the sidecar and metadata comes from the event log (the
  source of truth, invariant #1). Short-circuits for non-rehydratable
  backends before scanning the log.
- Wired into Main before the server accepts queries; no sidecar spawn.

Backfill of never-embedded history (needs the embedder) is a separate
follow-up. The in_memory non-durability warning already exists.
This commit is contained in:
2026-05-31 22:08:52 +04:00
parent d03479cea7
commit 5beb866036
5 changed files with 181 additions and 3 deletions
@@ -2,8 +2,8 @@ package com.correx.infrastructure.router.turbovec
import com.correx.core.router.l3.L3Hit
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.router.l3.RehydratableL3MemoryStore
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
@@ -15,14 +15,20 @@ import kotlin.io.path.createDirectories
* Adapter to TurboVec sidecar process for cross-session vector-based memory.
* Maintains in-memory metadata (sessionId, turnId, text, timestamp) since turbovec
* stores only vectors and IDs.
*
* The metadata map is volatile, but the sidecar vectors persist (via `persistPath`);
* on restart [rehydrateMetadata] rebuilds the map from the `ChatTurnEvent` log so
* queries resolve their hits again (invariant #1: the event log is authoritative).
*/
class TurboVecL3MemoryStore(private val config: TurboVecSidecarConfig) : L3MemoryStore {
class TurboVecL3MemoryStore(private val config: TurboVecSidecarConfig) : RehydratableL3MemoryStore {
private val json = Json { ignoreUnknownKeys = true }
private val mutex = Mutex()
private var process: Process? = null
private val metadata = java.util.concurrent.ConcurrentHashMap<String, L3MemoryEntry>()
// SCAFFOLDING: metadata persistence to be added with ChatTurnEvent
override suspend fun rehydrateMetadata(entries: List<L3MemoryEntry>) {
entries.forEach { metadata.putIfAbsent(it.id, it) }
}
override suspend fun store(entry: L3MemoryEntry) {
val request = SidecarRequest(