remem-ai 0.6.93

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::Result;

use super::*;

fn capture_codex_transcript_message(
    conn: &rusqlite::Connection,
    session_id: &str,
    role: &str,
    content: &str,
) -> Result<i64> {
    let outcome = db::record_captured_event(
        conn,
        &db::CaptureEventInput {
            host: "codex-cli",
            session_id,
            project: "/tmp/remem",
            cwd: None,
            event_type: "message",
            role: Some(role),
            tool_name: Some("codex-transcript"),
            content,
            task_kind: Some(db::ExtractionTaskKind::SessionRollup),
        },
    )?;
    Ok(outcome.event_row_id)
}

#[tokio::test]
async fn session_rollup_promotes_summary_claim_from_trusted_codex_user_event() -> Result<()> {
    let mut conn = setup_conn();
    let session_id = "sess-rollup-message-evidence";
    let request = "Fix summary evidence binding";
    let decision = "Transcript messages are captured as immutable evidence for summary promotion.";
    let candidate_text = format!("[Context: {request}]\n\n{decision}");
    let message_id = capture_codex_transcript_message(&conn, session_id, "user", &candidate_text)?;
    capture(
        &conn,
        session_id,
        "session_stop",
        &serde_json::json!({
            "session_id": session_id,
            "cwd": "/tmp/remem"
        })
        .to_string(),
    )?;
    let stop_id: i64 = conn.query_row(
        "SELECT id FROM captured_events
         WHERE session_id = ?1 AND event_type = 'session_stop'",
        [session_id],
        |row| row.get(0),
    )?;
    let task = claim_rollup_task(&mut conn)?;

    let result = process_with_summarizer(&mut conn, &task, |_prompt| async move {
        Ok(xml_response_with_structured_fields(
            "Promote summary claims from captured Codex message evidence.",
            request,
            decision,
            "",
            "",
            "",
            "",
        ))
    })
    .await?;
    assert_eq!(result, SessionRollupResult::Written);

    let (review_status, evidence_json, source_trust): (String, String, String) = conn.query_row(
        "SELECT review_status, evidence_event_ids, source_trust_class
         FROM memory_candidates
         WHERE text = ?1",
        [&candidate_text],
        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
    )?;
    assert_eq!(review_status, "auto_promoted");
    assert_eq!(
        serde_json::from_str::<Vec<i64>>(&evidence_json)?,
        vec![message_id]
    );
    assert!(!serde_json::from_str::<Vec<i64>>(&evidence_json)?.contains(&stop_id));
    assert_eq!(source_trust, "user_prompt");

    let memory_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM memories WHERE content = ?1",
        [&candidate_text],
        |row| row.get(0),
    )?;
    assert_eq!(memory_count, 1);
    Ok(())
}

#[tokio::test]
async fn session_rollup_keeps_codex_assistant_claim_review_gated() -> Result<()> {
    let mut conn = setup_conn();
    let session_id = "sess-rollup-assistant-external";
    let request = "Keep transcript provenance intact";
    let decision = "An assistant repeated an externally sourced claim.";
    let candidate_text = format!("[Context: {request}]\n\n{decision}");
    let message_id =
        capture_codex_transcript_message(&conn, session_id, "assistant", &candidate_text)?;
    capture(
        &conn,
        session_id,
        "session_stop",
        &serde_json::json!({
            "session_id": session_id,
            "cwd": "/tmp/remem"
        })
        .to_string(),
    )?;
    let task = claim_rollup_task(&mut conn)?;

    let result = process_with_summarizer(&mut conn, &task, |_prompt| async move {
        Ok(xml_response_with_structured_fields(
            "Preserve assistant transcript provenance.",
            request,
            decision,
            "",
            "",
            "",
            "",
        ))
    })
    .await?;
    assert_eq!(result, SessionRollupResult::Written);

    let (review_status, block_reason, evidence_json, source_trust): (
        String,
        String,
        String,
        String,
    ) = conn.query_row(
        "SELECT review_status, auto_promote_block_reason, evidence_event_ids,
                source_trust_class
         FROM memory_candidates
         WHERE text = ?1",
        [&candidate_text],
        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
    )?;
    assert_eq!(review_status, "pending_review");
    assert_eq!(block_reason, "source_trust_below_floor");
    assert_eq!(
        serde_json::from_str::<Vec<i64>>(&evidence_json)?,
        vec![message_id]
    );
    assert_eq!(source_trust, "external_content");

    let memory_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM memories WHERE content = ?1",
        [&candidate_text],
        |row| row.get(0),
    )?;
    assert_eq!(memory_count, 0);
    Ok(())
}

#[tokio::test]
async fn drained_user_prompt_remains_in_stop_transcript_evidence() -> Result<()> {
    let data_dir = crate::db::test_support::ScopedTestDataDir::new("rollup-drained-prompt");
    std::fs::create_dir_all(&data_dir.path)?;
    let transcript = data_dir.path.join("rollout.jsonl");
    let request = "Preserve the user's request after the prompt range was drained.";
    let answer = "The implementation and focused verification are complete.";
    let mut conn = setup_conn();
    let session_id = "drained-prompt-session";
    let task_id = capture(&conn, session_id, "user_prompt_submit", request)?;
    conn.execute(
        "UPDATE extraction_tasks SET status = 'done', cursor_event_id = high_watermark_event_id WHERE id = ?1",
        [task_id],
    )?;
    let message = |role: &str, text: &str| {
        serde_json::json!({
            "timestamp": "2026-09-02T00:00:00Z",
            "type": "response_item",
            "payload": {"type": "message", "role": role, "content": [{
                "type": if role == "user" { "input_text" } else { "output_text" },
                "text": text
            }]}
        })
    };
    std::fs::write(
        &transcript,
        format!(
            "{}\n{}\n",
            message("user", request),
            message("assistant", answer)
        ),
    )?;
    capture_codex_transcript_message(&conn, session_id, "assistant", answer)?;
    capture(
        &conn,
        session_id,
        "session_stop",
        &serde_json::json!({
            "session_id": session_id,
            "cwd": "/tmp/remem",
            "transcript_path": transcript,
            "transcript_byte_len": std::fs::metadata(&transcript)?.len()
        })
        .to_string(),
    )?;
    let task = claim_rollup_task(&mut conn)?;
    let range = load_rollup_range(&conn, &task)?.expect("follow-up range");
    assert!(range
        .events
        .iter()
        .all(|event| event.event_type != "user_prompt_submit"));
    let result = process_with_summarizer(&mut conn, &task, |prompt| async move {
        assert_eq!(
            prompt.matches(&request.replace('\'', "&apos;")).count(),
            1,
            "{prompt}"
        );
        assert_eq!(prompt.matches(answer).count(), 1, "{prompt}");
        Ok(xml_response("Completed the requested implementation.", ""))
    })
    .await?;
    assert_eq!(result, SessionRollupResult::Written);
    Ok(())
}