Skip to main content

rustyclaw_core/
memory_flush.rs

1//! Pre-compaction memory flush.
2//!
3//! Triggers a silent agent turn before compaction to persist durable memories.
4//! This prevents important context from being lost when conversation history
5//! is compacted to fit within the model's context window.
6
7use chrono::{Local, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Configuration for pre-compaction memory flush.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct MemoryFlushConfig {
13    /// Enable pre-compaction memory flush.
14    #[serde(default = "default_true")]
15    pub enabled: bool,
16
17    /// Trigger flush when this many tokens remain before hard limit.
18    /// Default: 4000 tokens before compaction threshold.
19    #[serde(default = "default_soft_threshold")]
20    pub soft_threshold_tokens: usize,
21
22    /// System prompt for flush turn.
23    #[serde(default = "default_flush_system_prompt")]
24    pub system_prompt: String,
25
26    /// User prompt for flush turn.
27    #[serde(default = "default_flush_user_prompt")]
28    pub user_prompt: String,
29}
30
31fn default_true() -> bool {
32    true
33}
34
35fn default_soft_threshold() -> usize {
36    4000
37}
38
39fn default_flush_system_prompt() -> String {
40    "Pre-compaction memory flush. Session context is approaching limits. \
41     Store any durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). \
42     IMPORTANT: If the file already exists, APPEND new content only — do not overwrite existing entries. \
43     If nothing important needs to be stored, reply with NO_REPLY."
44        .to_string()
45}
46
47fn default_flush_user_prompt() -> String {
48    "Write any lasting notes or context to memory files before compaction. \
49     Reply with NO_REPLY if nothing needs to be stored."
50        .to_string()
51}
52
53impl Default for MemoryFlushConfig {
54    fn default() -> Self {
55        Self {
56            enabled: true,
57            soft_threshold_tokens: default_soft_threshold(),
58            system_prompt: default_flush_system_prompt(),
59            user_prompt: default_flush_user_prompt(),
60        }
61    }
62}
63
64/// Memory flush controller.
65///
66/// Tracks whether a flush has been triggered in the current compaction cycle
67/// and provides methods to check if a flush is needed.
68pub struct MemoryFlush {
69    config: MemoryFlushConfig,
70    /// Track whether we've flushed this compaction cycle.
71    flushed_this_cycle: bool,
72}
73
74impl MemoryFlush {
75    /// Create a new memory flush controller.
76    pub fn new(config: MemoryFlushConfig) -> Self {
77        Self {
78            config,
79            flushed_this_cycle: false,
80        }
81    }
82
83    /// Check if we should trigger a flush based on token count.
84    ///
85    /// Returns `true` if:
86    /// - Memory flush is enabled
87    /// - We haven't already flushed this cycle
88    /// - Current token count exceeds the soft threshold
89    pub fn should_flush(
90        &self,
91        current_tokens: usize,
92        max_tokens: usize,
93        compaction_threshold: f64,
94    ) -> bool {
95        if !self.config.enabled || self.flushed_this_cycle {
96            return false;
97        }
98
99        // Calculate the threshold where we should flush
100        // (slightly before compaction would trigger)
101        let compaction_point = (max_tokens as f64 * compaction_threshold) as usize;
102        let flush_point = compaction_point.saturating_sub(self.config.soft_threshold_tokens);
103
104        current_tokens >= flush_point
105    }
106
107    /// Build the flush messages to inject.
108    ///
109    /// Returns (system_message, user_message) with date/time substituted.
110    pub fn build_flush_messages(&self) -> (String, String) {
111        let date = Local::now().format("%Y-%m-%d").to_string();
112        let time = Utc::now().format("%H:%M UTC").to_string();
113
114        let system = format!(
115            "{}\nCurrent time: {}. Today's date: {}.",
116            self.config.system_prompt, time, date
117        );
118
119        let user = self.config.user_prompt.replace("YYYY-MM-DD", &date);
120
121        (system, user)
122    }
123
124    /// Mark that we've flushed this cycle.
125    pub fn mark_flushed(&mut self) {
126        self.flushed_this_cycle = true;
127    }
128
129    /// Reset for a new compaction cycle.
130    pub fn reset_cycle(&mut self) {
131        self.flushed_this_cycle = false;
132    }
133
134    /// Check if flush is enabled.
135    pub fn is_enabled(&self) -> bool {
136        self.config.enabled
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_should_flush_at_threshold() {
146        let config = MemoryFlushConfig::default();
147        let flush = MemoryFlush::new(config);
148
149        // 100k max, 0.75 compaction threshold = 75k compaction point
150        // 75k - 4k soft threshold = 71k flush point
151        assert!(!flush.should_flush(70000, 100000, 0.75));
152        assert!(flush.should_flush(71000, 100000, 0.75));
153        assert!(flush.should_flush(75000, 100000, 0.75));
154    }
155
156    #[test]
157    fn test_flush_only_once_per_cycle() {
158        let config = MemoryFlushConfig::default();
159        let mut flush = MemoryFlush::new(config);
160
161        assert!(flush.should_flush(75000, 100000, 0.75));
162        flush.mark_flushed();
163        assert!(!flush.should_flush(75000, 100000, 0.75));
164
165        flush.reset_cycle();
166        assert!(flush.should_flush(75000, 100000, 0.75));
167    }
168
169    #[test]
170    fn test_disabled_flush() {
171        let config = MemoryFlushConfig {
172            enabled: false,
173            ..Default::default()
174        };
175        let flush = MemoryFlush::new(config);
176
177        assert!(!flush.should_flush(100000, 100000, 0.75));
178    }
179
180    #[test]
181    fn test_build_flush_messages() {
182        let config = MemoryFlushConfig::default();
183        let flush = MemoryFlush::new(config);
184
185        let (system, user) = flush.build_flush_messages();
186
187        assert!(system.contains("Pre-compaction memory flush"));
188        assert!(system.contains("UTC"));
189        assert!(!user.contains("YYYY-MM-DD")); // Should be substituted
190    }
191}