remem-ai 0.4.3

Persistent memory for Claude Code — single binary, zero subprocesses
Documentation
use std::collections::{HashMap, HashSet};

use crate::memory::Memory;

use super::super::format::{
    char_len, format_epoch_short, truncate_chars_with_ellipsis, type_label,
};
use super::super::policy::ContextLimits;

#[cfg(test)]
pub(in crate::context) fn render_memory_index(output: &mut String, memories: &[Memory]) -> usize {
    render_memory_index_with_limits(output, memories, &ContextLimits::default())
}

#[cfg(test)]
pub(in crate::context) fn render_memory_index_with_limits(
    output: &mut String,
    memories: &[Memory],
    limits: &ContextLimits,
) -> usize {
    let excluded_ids = HashSet::new();
    render_memory_index_with_limits_excluding(output, memories, limits, &excluded_ids)
}

pub(in crate::context) fn render_memory_index_with_limits_excluding(
    output: &mut String,
    memories: &[Memory],
    limits: &ContextLimits,
    excluded_ids: &HashSet<i64>,
) -> usize {
    if limits.memory_index_limit == 0 || limits.memory_index_char_limit == 0 {
        return 0;
    }

    let mut by_type: HashMap<&str, Vec<&Memory>> = HashMap::new();
    for memory in memories
        .iter()
        .filter(|memory| memory.memory_type != "preference")
        .filter(|memory| !excluded_ids.contains(&memory.id))
        .take(limits.memory_index_limit)
    {
        by_type
            .entry(memory.memory_type.as_str())
            .or_default()
            .push(memory);
    }
    if by_type.is_empty() {
        return 0;
    }

    let display_order = [
        "decision",
        "bugfix",
        "architecture",
        "discovery",
        "session_activity",
    ];

    let mut body = String::new();
    let mut total_chars = 0usize;
    let mut rendered_count = 0usize;
    for memory_type in &display_order {
        if let Some(memories_for_type) = by_type.get(memory_type) {
            if total_chars >= limits.memory_index_char_limit {
                break;
            }
            rendered_count += push_memory_index_line(
                &mut body,
                type_label(memory_type),
                memory_type,
                memories_for_type,
                limits.memory_index_char_limit,
                &mut total_chars,
            );
        }
    }

    for (memory_type, memories_for_type) in &by_type {
        if !display_order.contains(memory_type) && total_chars < limits.memory_index_char_limit {
            rendered_count += push_memory_index_line(
                &mut body,
                memory_type,
                memory_type,
                memories_for_type,
                limits.memory_index_char_limit,
                &mut total_chars,
            );
        }
    }
    if rendered_count == 0 {
        return 0;
    }
    output.push_str("## Index\n");
    output.push_str(&body);
    output.push('\n');
    rendered_count
}

fn push_memory_index_line(
    output: &mut String,
    label: &str,
    memory_type: &str,
    memories: &[&Memory],
    max_chars: usize,
    total_chars: &mut usize,
) -> usize {
    let section_label = if label == memory_type {
        memory_type.to_string()
    } else {
        label.to_string()
    };
    let prefix = format!("**{}** ({}): ", section_label, memories.len());
    let prefix_len = char_len(&prefix);
    if *total_chars + prefix_len >= max_chars {
        return 0;
    }
    let mut line = prefix;
    let mut line_chars = prefix_len;

    let mut rendered = 0usize;
    let mut first = true;
    for memory in memories.iter().take(10) {
        let date = format_epoch_short(memory.updated_at_epoch);
        let item = format!("#{} {} ({})", memory.id, memory.title, date);
        let separator = if first { "" } else { " | " };
        let next_len = char_len(separator) + char_len(&item);
        if *total_chars + line_chars + next_len > max_chars {
            if !first {
                break;
            }
            let remaining =
                max_chars.saturating_sub(*total_chars + line_chars + char_len(separator));
            if remaining == 0 {
                break;
            }
            let truncated = truncate_chars_with_ellipsis(&item, remaining);
            if truncated.is_empty() {
                break;
            }
            line.push_str(separator);
            line.push_str(&truncated);
            line_chars += char_len(separator) + char_len(&truncated);
            rendered += 1;
            break;
        }
        line.push_str(separator);
        line.push_str(&item);
        line_chars += next_len;
        rendered += 1;
        first = false;
    }
    if rendered == 0 {
        return 0;
    }
    if *total_chars + line_chars < max_chars {
        line.push('\n');
        line_chars += 1;
    }
    output.push_str(&line);
    *total_chars += line_chars;
    rendered
}