remem-ai 0.6.50

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use sha2::{Digest, Sha256};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DreamClusterMemberSnapshot {
    pub id: i64,
    pub version: i64,
    pub updated_at_epoch: i64,
    pub topic_key: Option<String>,
    pub title: String,
    pub content: String,
}

pub(crate) enum DreamDecisionPayload<'a> {
    Merge {
        topic_key: &'a str,
        memory_type: &'a str,
        title: &'a str,
        content: &'a str,
        intended_superseded_ids: &'a [i64],
    },
    NoMerge {
        reason: &'a str,
    },
    Conflict {
        conflicting_ids: &'a [i64],
        reason: &'a str,
    },
}

pub(crate) fn decision_payload_sha256(payload: DreamDecisionPayload<'_>) -> String {
    let mut hasher = Sha256::new();
    feed(&mut hasher, "dream-decision-payload-v1");
    match payload {
        DreamDecisionPayload::Merge {
            topic_key,
            memory_type,
            title,
            content,
            intended_superseded_ids,
        } => {
            feed(&mut hasher, "merge");
            feed(&mut hasher, topic_key);
            feed(&mut hasher, memory_type);
            feed(&mut hasher, title);
            feed(&mut hasher, content);
            feed_ids(&mut hasher, intended_superseded_ids);
        }
        DreamDecisionPayload::NoMerge { reason } => {
            feed(&mut hasher, "no_merge");
            feed(&mut hasher, reason);
            feed_ids(&mut hasher, &[]);
        }
        DreamDecisionPayload::Conflict {
            conflicting_ids,
            reason,
        } => {
            feed(&mut hasher, "conflict");
            feed(&mut hasher, reason);
            feed_ids(&mut hasher, conflicting_ids);
        }
    }
    format!("{:x}", hasher.finalize())
}

pub(crate) fn cluster_signature_sha256(
    project: &str,
    memory_type: &str,
    members: &[DreamClusterMemberSnapshot],
) -> String {
    let mut sorted = members.iter().collect::<Vec<_>>();
    sorted.sort_by_key(|member| member.id);

    let mut hasher = Sha256::new();
    feed(&mut hasher, "dream-cluster-v2");
    feed(&mut hasher, project);
    feed(&mut hasher, memory_type);
    feed(&mut hasher, &sorted.len().to_string());
    for member in sorted {
        feed(&mut hasher, &member.id.to_string());
        feed(&mut hasher, &member.version.to_string());
        feed(&mut hasher, &member.updated_at_epoch.to_string());
        feed_optional(&mut hasher, member.topic_key.as_deref());
        feed(&mut hasher, &member.title);
        feed(&mut hasher, &member.content);
    }
    format!("sha256:{:x}", hasher.finalize())
}

pub(crate) fn quarantine_semantic_discriminator_sha256(
    cluster_signature: &str,
    decision_payload_sha256: &str,
    generated_field: &str,
    pattern_id: &str,
    pattern_version: i64,
) -> String {
    let mut hasher = Sha256::new();
    feed(&mut hasher, "dream-quarantine-semantic-v1");
    feed(&mut hasher, cluster_signature);
    feed(&mut hasher, decision_payload_sha256);
    feed(&mut hasher, generated_field);
    feed(&mut hasher, pattern_id);
    feed(&mut hasher, &pattern_version.to_string());
    format!("{:x}", hasher.finalize())
}

fn feed_ids(hasher: &mut Sha256, ids: &[i64]) {
    hasher.update((ids.len() as u64).to_be_bytes());
    for id in ids {
        feed(hasher, &id.to_string());
    }
}

fn feed(hasher: &mut Sha256, value: &str) {
    hasher.update((value.len() as u64).to_be_bytes());
    hasher.update(value.as_bytes());
}

fn feed_optional(hasher: &mut Sha256, value: Option<&str>) {
    match value {
        Some(value) => {
            hasher.update([1]);
            feed(hasher, value);
        }
        None => hasher.update([0]),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn payload_hash_binds_field_boundaries_and_conflict_ids() {
        let first = decision_payload_sha256(DreamDecisionPayload::Merge {
            topic_key: "topic",
            memory_type: "decision",
            title: "a\nb",
            content: "c",
            intended_superseded_ids: &[1, 2],
        });
        let changed_boundary = decision_payload_sha256(DreamDecisionPayload::Merge {
            topic_key: "topic",
            memory_type: "decision",
            title: "a",
            content: "b\nc",
            intended_superseded_ids: &[1, 2],
        });
        let changed_ids = decision_payload_sha256(DreamDecisionPayload::Conflict {
            conflicting_ids: &[1, 3],
            reason: "same reason",
        });
        let original_ids = decision_payload_sha256(DreamDecisionPayload::Conflict {
            conflicting_ids: &[1, 2],
            reason: "same reason",
        });

        assert_ne!(first, changed_boundary);
        assert_ne!(changed_ids, original_ids);
    }

    #[test]
    fn cluster_hash_binds_member_version_and_canonical_payload() {
        let base = DreamClusterMemberSnapshot {
            id: 1,
            version: 1,
            updated_at_epoch: 100,
            topic_key: Some("topic".to_string()),
            title: "title".to_string(),
            content: "content".to_string(),
        };
        let original = cluster_signature_sha256("project", "decision", std::slice::from_ref(&base));
        let mut changed_version = base.clone();
        changed_version.version += 1;
        let mut changed_content = base;
        changed_content.content.push_str(" changed");

        assert_ne!(
            original,
            cluster_signature_sha256("project", "decision", &[changed_version])
        );
        assert_ne!(
            original,
            cluster_signature_sha256("project", "decision", &[changed_content])
        );
    }

    #[test]
    fn quarantine_semantic_hash_binds_pattern_and_decision() {
        let original = quarantine_semantic_discriminator_sha256(
            "sha256:cluster",
            "decision-a",
            "dream.title",
            "pattern",
            1,
        );
        assert_ne!(
            original,
            quarantine_semantic_discriminator_sha256(
                "sha256:cluster",
                "decision-b",
                "dream.title",
                "pattern",
                1,
            )
        );
        assert_ne!(
            original,
            quarantine_semantic_discriminator_sha256(
                "sha256:cluster",
                "decision-a",
                "dream.title",
                "pattern",
                2,
            )
        );
    }
}