remem-ai 0.6.10

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use super::super::invocation::ContextInvocation;
use super::super::policy::{ContextLimits, ContextPolicy, SectionKind};
use super::truncation::truncate_context_body_at_stable_boundary;
use super::{ContextRenderStats, Result};

pub(super) fn section_render_limits(policy: &ContextPolicy) -> ContextLimits {
    let mut limits = policy.limits;
    limits.core_item_limit = policy.section_item_limit(SectionKind::Core, limits.core_item_limit);
    limits.core_char_limit = policy.section_char_limit(SectionKind::Core, limits.core_char_limit);
    limits.memory_index_limit =
        policy.section_item_limit(SectionKind::MemoryIndex, limits.memory_index_limit);
    limits.memory_index_char_limit =
        policy.section_char_limit(SectionKind::MemoryIndex, limits.memory_index_char_limit);
    limits
}

#[cfg(test)]
pub(in crate::context) fn build_context_stats_footer(stats: &ContextRenderStats) -> String {
    build_context_stats_footer_with_style(stats, false)
}

pub(super) fn build_context_stats_footer_with_style(
    stats: &ContextRenderStats,
    use_colors: bool,
) -> String {
    super::super::style::context_stats_footer(stats, use_colors)
}

pub(super) fn context_source_note(source: Option<&str>) -> Option<&'static str> {
    match source?.trim().to_ascii_lowercase().as_str() {
        "compact" => Some("Codex compacted the chat, so remem refreshed memory context."),
        "clear" => Some("Context was reloaded after an explicit clear."),
        _ => None,
    }
}

pub(super) fn context_debug_enabled() -> bool {
    std::env::var("REMEM_CONTEXT_DEBUG")
        .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
        .unwrap_or(false)
}

#[cfg(test)]
pub(in crate::context) fn enforce_total_char_limit(output: &mut String, char_limit: usize) {
    enforce_total_char_limit_preserving_footer(output, char_limit, "");
}

pub(in crate::context) fn enforce_total_char_limit_preserving_footer(
    output: &mut String,
    char_limit: usize,
    footer: &str,
) -> usize {
    if char_limit == 0 || output.chars().count() <= char_limit {
        return output
            .strip_suffix(footer)
            .unwrap_or(output.as_str())
            .chars()
            .count();
    }

    let marker = "\n[remem context truncated to REMEM_CONTEXT_TOTAL_CHAR_LIMIT]\n";
    let marker_chars = marker.chars().count();
    let footer_chars = footer.chars().count();

    if !footer.is_empty() && output.ends_with(footer) && marker_chars + footer_chars <= char_limit {
        let keep_chars = char_limit - marker_chars - footer_chars;
        let body = output.strip_suffix(footer).unwrap_or(output.as_str());
        let mut truncated = truncate_context_body_at_stable_boundary(body, keep_chars);
        truncated.push_str(marker);
        truncated.push_str(footer);
        let retained_body_chars = truncated.chars().count() - marker_chars - footer_chars;
        *output = truncated;
        return retained_body_chars;
    }

    if marker_chars >= char_limit {
        *output = marker.chars().take(char_limit).collect();
        return 0;
    }

    let keep_chars = char_limit - marker_chars;
    let mut truncated = truncate_context_body_at_stable_boundary(output, keep_chars);
    let retained_body_chars = truncated.chars().count();
    truncated.push_str(marker);
    *output = truncated;
    retained_body_chars
}

pub(super) fn build_context_header_with_style(
    project: &str,
    current_branch: Option<&str>,
    hook_source: Option<&str>,
    host: super::super::host::HostKind,
    use_colors: bool,
) -> String {
    super::super::style::context_header(project, current_branch, hook_source, host, use_colors)
}

pub(in crate::context) fn context_stdout_for_invocation(
    output: &str,
    invocation: &ContextInvocation,
) -> Result<String> {
    if output.is_empty() || !is_codex_session_start_hook(invocation) {
        return Ok(output.to_string());
    }

    let additional_context = super::super::style::strip_ansi(output);
    let hook_output = serde_json::json!({
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": additional_context,
        }
    });
    Ok(format!("{}\n", serde_json::to_string(&hook_output)?))
}

fn is_codex_session_start_hook(invocation: &ContextInvocation) -> bool {
    if invocation.host != super::super::host::HostKind::CodexCli {
        return false;
    }

    matches!(
        invocation
            .source
            .as_deref()
            .map(|source| source.trim().to_ascii_lowercase()),
        Some(source)
            if matches!(source.as_str(), "startup" | "resume" | "clear" | "compact")
    )
}