Skip to main content

ai_agent/memory/
memdir.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/memdir/memdir.ts
2//! Memory directory management - translated from memdir/memdir.ts
3//!
4//! Provides the memory system prompt building and management.
5
6use crate::memory::memdir_paths::{
7    ensure_memory_dir_exists, get_auto_mem_entrypoint, get_auto_mem_path, is_auto_memory_enabled,
8};
9use crate::memory::types::EntrypointTruncation;
10
11/// Entrypoint filename
12pub const ENTRYPOINT_NAME: &str = "MEMORY.md";
13
14/// Maximum lines in MEMORY.md entrypoint (~125 chars/line at 200 lines)
15pub const MAX_ENTRYPOINT_LINES: usize = 200;
16
17/// Maximum bytes in MEMORY.md entrypoint (~25KB - catches long-line indexes)
18pub const MAX_ENTRYPOINT_BYTES: usize = 25_000;
19
20/// Shared guidance text appended to each memory directory prompt line.
21/// Shipped because Claude was burning turns on `ls`/`mkdir -p` before writing.
22/// Harness guarantees the directory exists via ensure_memory_dir_exists().
23pub const DIR_EXISTS_GUIDANCE: &str =
24    "This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence).";
25
26/// Truncate MEMORY.md content to the line AND byte caps, appending a warning
27/// that names which cap fired. Line-truncates first (natural boundary), then
28/// byte-truncates at the last newline before the cap so we don't cut mid-line.
29pub fn truncate_entrypoint_content(raw: &str) -> EntrypointTruncation {
30    let trimmed = raw.trim();
31    let content_lines: Vec<&str> = trimmed.lines().collect();
32    let line_count = content_lines.len();
33    let byte_count = trimmed.len();
34
35    let was_line_truncated = line_count > MAX_ENTRYPOINT_LINES;
36    // Check original byte count — long lines are the failure mode the byte cap
37    // targets, so post-line-truncation size would understate the warning.
38    let was_byte_truncated = byte_count > MAX_ENTRYPOINT_BYTES;
39
40    if !was_line_truncated && !was_byte_truncated {
41        return EntrypointTruncation {
42            content: trimmed.to_string(),
43            line_count,
44            byte_count,
45            was_line_truncated,
46            was_byte_truncated,
47        };
48    }
49
50    let truncated = if was_line_truncated {
51        content_lines[..MAX_ENTRYPOINT_LINES].join("\n")
52    } else {
53        trimmed.to_string()
54    };
55
56    let truncated = if truncated.len() > MAX_ENTRYPOINT_BYTES {
57        if let Some(cut_at) = truncated.rfind('\n') {
58            if cut_at > 0 {
59                truncated[..cut_at].to_string()
60            } else {
61                truncated[..MAX_ENTRYPOINT_BYTES].to_string()
62            }
63        } else {
64            truncated[..MAX_ENTRYPOINT_BYTES].to_string()
65        }
66    } else {
67        truncated
68    };
69
70    let reason = if was_byte_truncated && !was_line_truncated {
71        format!(
72            "{} (limit: {} bytes) — index entries are too long",
73            format_file_size(byte_count),
74            format_file_size(MAX_ENTRYPOINT_BYTES)
75        )
76    } else if was_line_truncated && !was_byte_truncated {
77        format!("{} lines (limit: {})", line_count, MAX_ENTRYPOINT_LINES)
78    } else {
79        format!("{} lines and {}", line_count, format_file_size(byte_count))
80    };
81
82    let content = format!(
83        "{}\n\n> WARNING: {} is {}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.",
84        truncated, ENTRYPOINT_NAME, reason
85    );
86
87    EntrypointTruncation {
88        content,
89        line_count,
90        byte_count,
91        was_line_truncated,
92        was_byte_truncated,
93    }
94}
95
96/// Simple file size formatter (equivalent to TypeScript formatFileSize)
97fn format_file_size(bytes: usize) -> String {
98    if bytes >= 1_000_000 {
99        format!("{:.1}M", bytes as f64 / 1_000_000.0)
100    } else if bytes >= 1_000 {
101        format!("{:.1}K", bytes as f64 / 1_000.0)
102    } else {
103        format!("{}B", bytes)
104    }
105}
106
107/// Build the typed-memory behavioral instructions (without MEMORY.md content).
108/// Constrains memories to a closed four-type taxonomy (user / feedback / project /
109/// reference) — content that is derivable from the current project state (code
110/// patterns, architecture, git history) is explicitly excluded.
111pub fn build_memory_lines(
112    display_name: &str,
113    memory_dir: &str,
114    extra_guidelines: Option<&[&str]>,
115    skip_index: bool,
116) -> Vec<String> {
117    let how_to_save = if skip_index {
118        vec![
119            "## How to save memories".to_string(),
120            String::new(),
121            "Write each memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:".to_string(),
122            String::new(),
123        ]
124        .into_iter()
125        .chain(MEMORY_FRONTMATTER_EXAMPLE.iter().map(|s| s.to_string()))
126        .chain(vec![
127            String::new(),
128            "- Keep the name, description, and type fields in memory files up-to-date with the content".to_string(),
129            "- Organize memory semantically by topic, not chronologically".to_string(),
130            "- Update or remove memories that turn out to be wrong or outdated".to_string(),
131            "- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.".to_string(),
132        ])
133        .collect::<Vec<_>>()
134    } else {
135        vec![
136            "## How to save memories".to_string(),
137            String::new(),
138            "Saving a memory is a two-step process:".to_string(),
139            String::new(),
140            "**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:".to_string(),
141            String::new(),
142        ]
143        .into_iter()
144        .chain(MEMORY_FRONTMATTER_EXAMPLE.iter().map(|s| s.to_string()))
145        .chain(vec![
146            String::new(),
147            format!("**Step 2** — add a pointer to that file in `{}`. `{}` is an index, not a memory — each entry should be one line, under ~150 characters: `- [Title](file.md) — one-line hook`. It has no frontmatter. Never write memory content directly into `{}`.", ENTRYPOINT_NAME, ENTRYPOINT_NAME, ENTRYPOINT_NAME),
148            String::new(),
149            format!("- `{}` is always loaded into your conversation context — lines after {} will be truncated, so keep the index concise", ENTRYPOINT_NAME, MAX_ENTRYPOINT_LINES),
150            "- Keep the name, description, and type fields in memory files up-to-date with the content".to_string(),
151            "- Organize memory semantically by topic, not chronologically".to_string(),
152            "- Update or remove memories that turn out to be wrong or outdated".to_string(),
153            "- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.".to_string(),
154        ])
155        .collect::<Vec<_>>()
156    };
157
158    let mut lines = vec![
159        format!("# {}", display_name),
160        String::new(),
161        format!(
162            "You have a persistent, file-based memory system at `{}`. {}",
163            memory_dir, DIR_EXISTS_GUIDANCE
164        ),
165        String::new(),
166        "You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.".to_string(),
167        String::new(),
168        "If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.".to_string(),
169        String::new(),
170    ];
171
172    // Types section
173    lines.extend(TYPES_SECTION_INDIVIDUAL.iter().map(|s| s.to_string()));
174    lines.push(String::new());
175
176    // What NOT to save
177    lines.extend(WHAT_NOT_TO_SAVE_SECTION.iter().map(|s| s.to_string()));
178    lines.push(String::new());
179
180    // How to save
181    lines.extend(how_to_save);
182    lines.push(String::new());
183
184    // When to access
185    lines.extend(WHEN_TO_ACCESS_SECTION.iter().map(|s| s.to_string()));
186    lines.push(String::new());
187
188    // Trusting recall
189    lines.extend(TRUSTING_RECALL_SECTION.iter().map(|s| s.to_string()));
190    lines.push(String::new());
191
192    // Memory and other forms of persistence
193    lines.push("## Memory and other forms of persistence".to_string());
194    lines.push("Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.".to_string());
195    lines.push("- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.".to_string());
196    lines.push("- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.".to_string());
197    lines.push(String::new());
198
199    // Extra guidelines
200    if let Some(guidelines) = extra_guidelines {
201        lines.extend(guidelines.iter().map(|s| s.to_string()));
202        lines.push(String::new());
203    }
204
205    // Searching past context section (simplified - would integrate with growthbook feature flags)
206    lines.extend(build_searching_past_context_section(memory_dir));
207
208    lines
209}
210
211/// Frontmatter example for memory files
212const MEMORY_FRONTMATTER_EXAMPLE: &[&str] = &[
213    "```markdown",
214    "---",
215    "name: {{memory name}}",
216    "description: {{one-line description — used to decide relevance in future conversations, so be specific}}",
217    "type: {{user, feedback, project, reference}}",
218    "---",
219    "",
220    "{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}",
221    "```",
222];
223
224/// Types section content
225const TYPES_SECTION_INDIVIDUAL: &[&str] = &[
226    "## Types of memory",
227    "",
228    "There are several discrete types of memory that you can store in your memory system:",
229    "",
230    "<types>",
231    "<type>",
232    "    <name>user</name>",
233    "    <description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically.</description>",
234    "    <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>",
235    "    <how_to_use>When your work should be informed by the user's profile or perspective.</how_to_use>",
236    "</type>",
237    "<type>",
238    "    <name>feedback</name>",
239    "    <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing.</description>",
240    "    <when_to_save>Any time the user corrects your approach (\"no not that\", \"don't\", \"stop doing X\") OR confirms a non-obvious approach worked.</when_to_save>",
241    "    <how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>",
242    "</type>",
243    "<type>",
244    "    <name>project</name>",
245    "    <description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history.</description>",
246    "    <when_to_save>When you learn who is doing what, why, or by when.</when_to_save>",
247    "    <how_to_use>Use these memories to more fully understand the details and nuance behind the user's request.</how_to_use>",
248    "</type>",
249    "<type>",
250    "    <name>reference</name>",
251    "    <description>Stores pointers to where information can be found in external systems.</description>",
252    "    <when_to_save>When you learn about resources in external systems and their purpose.</when_to_save>",
253    "    <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>",
254    "</type>",
255    "</types>",
256    "",
257];
258
259/// What NOT to save section content
260const WHAT_NOT_TO_SAVE_SECTION: &[&str] = &[
261    "## What NOT to save in memory",
262    "",
263    "- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.",
264    "- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.",
265    "- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.",
266    "- Anything already documented in AI.md files.",
267    "- Ephemeral task details: in-progress work, temporary state, current conversation context.",
268    "",
269    "These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.",
270];
271
272/// When to access section content
273const WHEN_TO_ACCESS_SECTION: &[&str] = &[
274    "## When to access memories",
275    "- When memories seem relevant, or the user references prior-conversation work.",
276    "- You MUST access memory when the user explicitly asks you to check, recall, or remember.",
277    "- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty.",
278    "- Memory records can become stale over time. Verify that the memory is still correct and up-to-date.",
279];
280
281/// Trusting recall section content
282const TRUSTING_RECALL_SECTION: &[&str] = &[
283    "## Before recommending from memory",
284    "",
285    "A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:",
286    "",
287    "- If the memory names a file path: check the file exists.",
288    "- If the memory names a function or flag: grep for it.",
289    "- If the user is about to act on your recommendation (not just asking about history), verify first.",
290    "",
291    "\"The memory says X exists\" is not the same as \"X exists now.\"",
292    "",
293    "A memory that summarizes repo state (activity logs, architecture snapshots) is frozen in time. If the user asks about *recent* or *current* state, prefer `git log` or reading the code over recalling the snapshot.",
294];
295
296/// Build the "Searching past context" section
297fn build_searching_past_context_section(auto_mem_dir: &str) -> Vec<String> {
298    // Simplified version - would integrate with growthbook feature flags
299    // In full implementation, this would check getFeatureValue_CACHED_MAY_BE_STALE('tengu_coral_fern', false)
300    vec![]
301}
302
303/// Build the memory prompt with MEMORY.md content included.
304/// Used by agent memory.
305pub fn build_memory_prompt(params: BuildMemoryPromptParams) -> String {
306    let BuildMemoryPromptParams {
307        display_name,
308        extra_guidelines,
309    } = params;
310
311    let memory_dir = get_auto_mem_path();
312    let memory_dir_str = memory_dir.to_string_lossy();
313
314    // Read existing memory entrypoint
315    let entrypoint_path = get_auto_mem_entrypoint();
316    let entrypoint_content = if entrypoint_path.exists() {
317        std::fs::read_to_string(&entrypoint_path).unwrap_or_default()
318    } else {
319        String::new()
320    };
321
322    let mut lines = build_memory_lines(
323        &display_name,
324        &memory_dir_str,
325        extra_guidelines.as_deref(),
326        false,
327    );
328
329    if !entrypoint_content.trim().is_empty() {
330        let t = truncate_entrypoint_content(&entrypoint_content);
331        lines.push(format!("## {}", ENTRYPOINT_NAME));
332        lines.push(String::new());
333        lines.push(t.content);
334    } else {
335        lines.push(format!("## {}", ENTRYPOINT_NAME));
336        lines.push(String::new());
337        lines.push(format!(
338            "Your {} is currently empty. When you save new memories, they will appear here.",
339            ENTRYPOINT_NAME
340        ));
341    }
342
343    lines.join("\n")
344}
345
346/// Parameters for build_memory_prompt
347pub struct BuildMemoryPromptParams<'a> {
348    pub display_name: &'a str,
349    pub extra_guidelines: Option<Vec<&'a str>>,
350}
351
352/// Load the unified memory prompt for inclusion in the system prompt.
353/// Returns None when auto memory is disabled.
354pub async fn load_memory_prompt() -> Option<String> {
355    if !is_auto_memory_enabled() {
356        return None;
357    }
358
359    let auto_dir = get_auto_mem_path();
360    // Ensure the directory exists
361    ensure_memory_dir_exists(&auto_dir).ok()?;
362
363    Some(build_memory_prompt(BuildMemoryPromptParams {
364        display_name: "auto memory",
365        extra_guidelines: None,
366    }))
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_truncate_entrypoint_content_no_truncation() {
375        let content = "line 1\nline 2\nline 3";
376        let result = truncate_entrypoint_content(content);
377
378        assert_eq!(result.content, content);
379        assert!(!result.was_line_truncated);
380        assert!(!result.was_byte_truncated);
381    }
382
383    #[test]
384    fn test_truncate_entrypoint_content_line_truncation() {
385        let content: String = (0..=MAX_ENTRYPOINT_LINES)
386            .map(|i| format!("line {}\n", i))
387            .collect();
388        let result = truncate_entrypoint_content(&content);
389
390        assert!(result.was_line_truncated);
391        assert!(result.content.contains("WARNING: MEMORY.md is"));
392    }
393
394    #[test]
395    fn test_build_memory_lines() {
396        let lines = build_memory_lines("auto memory", "/tmp/memory", None, false);
397        assert!(!lines.is_empty());
398        assert!(lines.iter().any(|l| l.contains("Types of memory")));
399        assert!(lines.iter().any(|l| l.contains("How to save memories")));
400    }
401
402    #[test]
403    fn test_build_memory_lines_skip_index() {
404        let lines = build_memory_lines("auto memory", "/tmp/memory", None, true);
405        assert!(!lines.iter().any(|l| l.contains("Step 1")));
406        assert!(!lines.iter().any(|l| l.contains("Step 2")));
407    }
408
409    #[test]
410    fn test_build_memory_prompt() {
411        let prompt = build_memory_prompt(BuildMemoryPromptParams {
412            display_name: "auto memory",
413            extra_guidelines: None,
414        });
415        assert!(prompt.contains("auto memory"));
416        assert!(prompt.contains("MEMORY.md"));
417    }
418}