velesdb-memory 0.14.0

VelesDB-memory: local-first MCP memory server for AI agents (remember/recall/relate/forget/why + deterministic context compiler).
Documentation
use std::path::Path;

use super::LiveGenerationSlot;
use crate::embedder::Embedder;
use crate::migration::{
    commit_retained_switch, finalize_staged_live_switch, rollback_staged_live_switch,
    target_embedder_witness, ARCHIVE_SUFFIX,
};
use crate::mutation::controller::{ControllerPhase, ConvergenceController};
use crate::mutation::journal::{CutoverIdentity, DirtyJournal};
use crate::{MemoryError, MemoryService};

pub(crate) struct LiveRecovery<'a> {
    pub(crate) controller: &'a mut ConvergenceController,
    pub(crate) journal: &'a DirtyJournal,
    pub(crate) source: &'a Path,
    pub(crate) destination: &'a Path,
    pub(crate) source_model: &'a str,
    pub(crate) target_model: &'a str,
}

impl<E: Embedder> LiveGenerationSlot<E> {
    pub(crate) fn recover(
        mut recovery: LiveRecovery<'_>,
        source_embedder: E,
        target_embedder: E,
    ) -> Result<Self, MemoryError> {
        verify_identity(&recovery, &target_embedder)?;
        match recovery.controller.phase() {
            ControllerPhase::Quiescing { .. } => recover_source(&mut recovery, source_embedder),
            ControllerPhase::Activated => recover_target(&mut recovery, target_embedder),
            _ => Err(super::unavailable(
                "controller does not require live cutover recovery",
            )),
        }
    }
}

fn verify_identity<E: Embedder>(
    recovery: &LiveRecovery<'_>,
    target_embedder: &E,
) -> Result<(), MemoryError> {
    let witness = target_embedder_witness(target_embedder)?;
    recovery.journal.verify_cutover_identity(&CutoverIdentity {
        source: recovery.source,
        destination: recovery.destination,
        source_provenance: recovery.source_model,
        target_model: recovery.target_model,
        target_dimension: target_embedder.dimension(),
        target_witness: &witness,
        epoch_id: recovery.controller.epoch_id(),
    })
}

fn recover_source<E: Embedder>(
    recovery: &mut LiveRecovery<'_>,
    source_embedder: E,
) -> Result<LiveGenerationSlot<E>, MemoryError> {
    let archive = recovery.source.with_file_name(format!(
        "{}{}",
        recovery
            .source
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or_else(|| super::unavailable("source has no usable directory name"))?,
        ARCHIVE_SUFFIX
    ));
    if archive.exists() || !recovery.destination.exists() {
        rollback_staged_live_switch(recovery.source, recovery.destination)?;
    }
    let service = MemoryService::open(recovery.source, source_embedder)?;
    recovery.controller.complete_source_recovery()?;
    Ok(LiveGenerationSlot::new(service, recovery.source_model))
}

fn recover_target<E: Embedder>(
    recovery: &mut LiveRecovery<'_>,
    target_embedder: E,
) -> Result<LiveGenerationSlot<E>, MemoryError> {
    let service = MemoryService::open(recovery.source, target_embedder)?;
    finalize_staged_live_switch(recovery.source, recovery.destination)?;
    commit_retained_switch(recovery.source, recovery.destination)?;
    recovery.controller.complete_target_recovery()?;
    Ok(LiveGenerationSlot::new(service, recovery.target_model))
}