Skip to main content

agent_code_lib/memory/
session_notes.rs

1//! Per-session working notes.
2//!
3//! Maintains a structured markdown file for each session with
4//! sections for current state, task spec, files, workflow, errors,
5//! and key results. Used as a cheap compaction source — when context
6//! needs shrinking, session notes serve as the summary without
7//! requiring an expensive API call.
8
9use std::path::PathBuf;
10
11/// Session notes template sections.
12const TEMPLATE: &str = "\
13# Session Notes
14
15## Current State
16_What is actively being worked on right now?_
17
18## Task
19_What did the user ask to do?_
20
21## Key Files
22_Important files and their relevance_
23
24## Workflow
25_Commands run and their results_
26
27## Errors
28_Errors encountered and how they were fixed_
29
30## Learnings
31_What worked well? What didn't?_
32
33## Results
34_Key outputs or deliverables_
35
36## Log
37_Step by step, what was attempted and done_
38";
39
40/// Get the path for this session's notes file.
41pub fn session_notes_path(session_id: &str) -> Option<PathBuf> {
42    let dir = dirs::config_dir()?.join("agent-code").join("session-notes");
43    let _ = std::fs::create_dir_all(&dir);
44    Some(dir.join(format!("{session_id}.md")))
45}
46
47/// Initialize session notes with the template if the file doesn't exist.
48pub fn init_session_notes(session_id: &str) -> Option<PathBuf> {
49    let path = session_notes_path(session_id)?;
50    if !path.exists() {
51        let _ = std::fs::write(&path, TEMPLATE);
52    }
53    Some(path)
54}
55
56/// Read existing session notes content.
57pub fn read_session_notes(session_id: &str) -> Option<String> {
58    let path = session_notes_path(session_id)?;
59    std::fs::read_to_string(path).ok()
60}
61
62/// Check if session notes have meaningful content (not just the template).
63pub fn has_content(session_id: &str) -> bool {
64    match read_session_notes(session_id) {
65        Some(content) => {
66            // Check if any section has been filled in (non-italic content).
67            content
68                .lines()
69                .any(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('_'))
70        }
71        None => false,
72    }
73}
74
75/// Build a prompt for updating session notes from recent conversation.
76pub fn build_update_prompt(session_id: &str, recent_messages: &str) -> String {
77    let existing = read_session_notes(session_id).unwrap_or_else(|| TEMPLATE.to_string());
78
79    format!(
80        "Update the session notes below based on the recent conversation. \
81         Fill in sections that apply, leave others with their placeholder text. \
82         Be concise — these notes are for quick reference, not detailed logs. \
83         Keep the markdown structure intact.\n\n\
84         Current notes:\n```\n{existing}\n```\n\n\
85         Recent conversation:\n{recent_messages}\n\n\
86         Write the updated session notes (full markdown document):"
87    )
88}
89
90/// Clean up old session notes (older than 7 days).
91pub fn cleanup_old_notes() {
92    let dir = match dirs::config_dir() {
93        Some(d) => d.join("agent-code").join("session-notes"),
94        None => return,
95    };
96
97    if !dir.is_dir() {
98        return;
99    }
100
101    let cutoff = std::time::SystemTime::now() - std::time::Duration::from_secs(7 * 24 * 60 * 60);
102
103    if let Ok(entries) = std::fs::read_dir(&dir) {
104        for entry in entries.flatten() {
105            if let Ok(meta) = entry.metadata()
106                && let Ok(modified) = meta.modified()
107                && modified < cutoff
108            {
109                let _ = std::fs::remove_file(entry.path());
110            }
111        }
112    }
113}