weavatrix-memory 0.3.3

Event-sourced bitemporal evidence memory and budgeted context retrieval for coding agents
Documentation
use super::{
    CompactSnapshotCodec, HEADER_V1, ProjectionSnapshot, Result, Writer, retractions, write_cursor,
    write_fact, write_revision,
};
use crate::codec::Codec;
use crate::{
    domain::{Evidence, MemoryEvent, MemoryFact, MemoryNode},
    event::NewEvent,
    id::{AgentId, EntityId, EventId, FactId, SessionId, StreamId},
    projection::{MemoryProjection, ProjectionClock, replay_tracked},
    store::{EventStore, ExpectedVersion, InMemoryStore},
    time::Timestamp,
};

fn ts(value: i64) -> Timestamp {
    Timestamp::from_unix_micros(value)
}

fn encode_v1(value: &ProjectionSnapshot<MemoryProjection>) -> Result<Vec<u8>> {
    let mut writer = Writer::new(HEADER_V1);
    write_cursor(&mut writer, &value.cursor)?;
    writer.usize(value.projection.nodes.len())?;
    for history in &value.projection.nodes {
        writer.usize(history.later.len() + 1)?;
        write_revision(&mut writer, &history.first)?;
        for revision in &history.later {
            write_revision(&mut writer, revision)?;
        }
    }
    writer.usize(value.projection.facts.len())?;
    for fact in &value.projection.facts {
        write_fact(&mut writer, fact)?;
    }
    writer.usize(value.projection.supersessions.len())?;
    for (prior, change) in &value.projection.supersessions {
        writer.string(prior.as_str())?;
        writer.string(change.replacement.as_str())?;
        writer.signed(change.valid_from.as_unix_micros());
        writer.signed(change.recorded_at.as_unix_micros());
    }
    retractions::write_v1(&mut writer, &value.projection.retractions)?;
    writer.optional_u64(value.projection.last_global_position);
    Ok(writer.finish())
}

fn event(id: &str, recorded_at: i64, payload: MemoryEvent) -> NewEvent<MemoryEvent> {
    NewEvent::new(
        EventId::new(id).unwrap(),
        payload.event_type(),
        ts(recorded_at),
        ts(recorded_at),
        AgentId::new("agent:test").unwrap(),
        SessionId::new("session:test").unwrap(),
        payload,
    )
    .unwrap()
}

#[test]
fn v1_compact_snapshot_migrates_a_single_retraction() {
    let fact = MemoryFact::new(
        FactId::new("fact:old").unwrap(),
        EntityId::new("task:1").unwrap(),
        "affects",
        EntityId::new("file:1").unwrap(),
        ts(10),
        ts(20),
        AgentId::new("agent:test").unwrap(),
        SessionId::new("session:test").unwrap(),
        Evidence::new("test", "suite").unwrap(),
    )
    .unwrap();
    let pending = [
        event(
            "event:task",
            1,
            MemoryEvent::NodeUpserted {
                node: MemoryNode::new(EntityId::new("task:1").unwrap(), "task", "Task").unwrap(),
            },
        ),
        event(
            "event:file",
            1,
            MemoryEvent::NodeUpserted {
                node: MemoryNode::new(EntityId::new("file:1").unwrap(), "file", "File").unwrap(),
            },
        ),
        event(
            "event:fact",
            20,
            MemoryEvent::FactRecorded { fact: fact.clone() },
        ),
        event(
            "event:retract",
            50,
            MemoryEvent::FactRetracted {
                fact_id: fact.id,
                valid_until: ts(30),
                evidence: vec![Evidence::new("test", "suite").unwrap()],
            },
        ),
    ];
    let mut store = InMemoryStore::default();
    store
        .append(
            &StreamId::new("stream:retract").unwrap(),
            ExpectedVersion::NoStream,
            &pending,
        )
        .unwrap();
    let events = store.load_all(None, usize::MAX);
    let (projection, cursor) = replay_tracked::<_, MemoryProjection>(&events).unwrap();
    let snapshot = ProjectionSnapshot { cursor, projection };
    let bytes = encode_v1(&snapshot).unwrap();
    assert!(bytes.starts_with(HEADER_V1));
    let decoded = CompactSnapshotCodec.decode(&bytes).unwrap();
    assert!(
        decoded
            .projection
            .view(ProjectionClock::new(ts(40), ts(60)))
            .facts
            .is_empty()
    );
    assert_eq!(
        decoded
            .projection
            .view(ProjectionClock::new(ts(40), ts(40)))
            .facts
            .len(),
        1
    );
}