remem-ai 0.6.61

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use crate::memory::lesson::LessonMemory;
use crate::memory::MemoryStalenessLabel;
use std::collections::HashMap;

use super::super::audit::memory_render_metadata_with_labels;
use super::super::format::{
    char_len, format_epoch_short, inline_context_text, truncate_chars_with_ellipsis,
};

const PREVIEW_LEN: usize = 180;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(in crate::context) struct LessonRenderSummary {
    pub count: usize,
    pub ids: Vec<i64>,
    pub item_end_chars: Vec<usize>,
}

#[cfg(test)]
pub(in crate::context) fn render_lessons_with_limit(
    output: &mut String,
    lessons: &[LessonMemory],
    item_limit: usize,
    char_limit: usize,
) -> usize {
    render_lessons_with_summary(output, lessons, item_limit, char_limit).count
}

pub(in crate::context) fn render_lessons_with_limit_and_staleness(
    output: &mut String,
    lessons: &[LessonMemory],
    item_limit: usize,
    char_limit: usize,
    render_reference_epoch: i64,
    staleness_labels: &HashMap<i64, MemoryStalenessLabel>,
) -> usize {
    render_lessons_with_summary_and_staleness(
        output,
        lessons,
        item_limit,
        char_limit,
        render_reference_epoch,
        staleness_labels,
    )
    .count
}

#[cfg(test)]
pub(in crate::context) fn render_lessons_with_summary(
    output: &mut String,
    lessons: &[LessonMemory],
    item_limit: usize,
    char_limit: usize,
) -> LessonRenderSummary {
    render_lessons_with_summary_and_staleness(
        output,
        lessons,
        item_limit,
        char_limit,
        chrono::Utc::now().timestamp(),
        &HashMap::new(),
    )
}

pub(in crate::context) fn render_lessons_with_summary_and_staleness(
    output: &mut String,
    lessons: &[LessonMemory],
    item_limit: usize,
    char_limit: usize,
    render_reference_epoch: i64,
    staleness_labels: &HashMap<i64, MemoryStalenessLabel>,
) -> LessonRenderSummary {
    if lessons.is_empty() || item_limit == 0 || char_limit == 0 {
        return LessonRenderSummary::default();
    }
    let header = "## Lessons\n";
    let trailer_chars = 1;
    let mut total_chars = char_len(header) + trailer_chars;
    if total_chars >= char_limit {
        return LessonRenderSummary::default();
    }

    let output_start_chars = char_len(output);
    let mut body = String::new();
    let mut rendered = 0usize;
    let mut ids = Vec::new();
    let mut item_end_chars = Vec::new();
    for lesson in lessons.iter().take(item_limit) {
        let memory = &lesson.memory;
        let metadata = &lesson.metadata;
        let memory_title = inline_context_text(&memory.title);
        // GH-958: failure lessons carry an explicit guardrail marker so the
        // model reads them as "this approach failed before", not as guidance.
        let guardrail_prefix = if metadata.outcome_kind == "failure" {
            "guardrail — this failed before: "
        } else {
            ""
        };
        let title = format!(
            "**#{} {}{}** (confidence {:.2}, reinforced {}, {}; {})\n",
            memory.id,
            guardrail_prefix,
            memory_title,
            metadata.confidence,
            metadata.reinforcement_count,
            format_epoch_short(metadata.last_reinforced_at_epoch),
            memory_render_metadata_with_labels(memory, render_reference_epoch, staleness_labels)
        );
        let fixed_chars = char_len(&title) + 1;
        if total_chars + fixed_chars >= char_limit {
            break;
        }
        let preview_limit = (char_limit - total_chars - fixed_chars).min(PREVIEW_LEN);
        let preview_text = inline_context_text(&memory.text);
        let preview = truncate_chars_with_ellipsis(&preview_text, preview_limit);
        if preview.is_empty() {
            continue;
        }
        let item_chars = fixed_chars + char_len(&preview);
        if total_chars + item_chars > char_limit {
            break;
        }
        body.push_str(&title);
        body.push_str(&preview);
        body.push('\n');
        total_chars += item_chars;
        ids.push(memory.id);
        item_end_chars.push(output_start_chars + total_chars - trailer_chars);
        rendered += 1;
    }
    if rendered == 0 {
        return LessonRenderSummary::default();
    }
    output.push_str(header);
    output.push_str(&body);
    output.push('\n');
    LessonRenderSummary {
        count: rendered,
        ids,
        item_end_chars,
    }
}