Skip to main content

codemem_engine/hooks/
triggers.rs

1//! Trigger-based auto-insights generated during PostToolUse.
2
3/// An auto-insight generated by trigger-based analysis during PostToolUse.
4#[derive(Debug, Clone)]
5pub struct AutoInsight {
6    /// The insight content to store as a memory.
7    pub content: String,
8    /// Tags to attach to the insight memory.
9    pub tags: Vec<String>,
10    /// Importance score for the insight.
11    pub importance: f64,
12    /// Unique tag used for deduplication within a session.
13    pub dedup_tag: String,
14}
15
16/// Check trigger conditions against session activity and return any auto-insights.
17///
18/// Three triggers are evaluated:
19/// 1. **Directory focus**: 3+ files read from the same directory suggests deep exploration.
20/// 2. **Edit after read**: Editing a file that was previously read indicates an informed change.
21/// 3. **Repeated search**: Same search pattern used 2+ times suggests a recurring need.
22///
23/// Each trigger checks `has_auto_insight()` to avoid duplicate insights within the same session.
24pub fn check_triggers(
25    storage: &dyn codemem_core::StorageBackend,
26    session_id: &str,
27    tool_name: &str,
28    file_path: Option<&str>,
29    pattern: Option<&str>,
30) -> Vec<AutoInsight> {
31    let mut insights = Vec::new();
32
33    // Trigger 1: 3+ files read from the same directory
34    if tool_name == "Read" {
35        if let Some(fp) = file_path {
36            let directory = std::path::Path::new(fp)
37                .parent()
38                .map(|p| p.to_string_lossy().to_string())
39                .unwrap_or_default();
40            if !directory.is_empty() {
41                let dedup_tag = format!("dir_focus:{}", directory);
42                let already_exists = storage
43                    .has_auto_insight(session_id, &dedup_tag)
44                    .unwrap_or(true);
45                if !already_exists {
46                    let count = storage
47                        .count_directory_reads(session_id, &directory)
48                        .unwrap_or(0);
49                    if count >= 3 {
50                        insights.push(AutoInsight {
51                            content: format!(
52                                "Deep exploration of directory '{}': {} files read in this session. \
53                                 This area may be a focus of the current task.",
54                                directory, count
55                            ),
56                            tags: vec![
57                                "auto-insight".to_string(),
58                                "directory-focus".to_string(),
59                                format!("dir:{}", directory),
60                            ],
61                            importance: 0.6,
62                            dedup_tag,
63                        });
64                    }
65                }
66            }
67        }
68    }
69
70    // Trigger 2: Edit after read — an informed change
71    if matches!(tool_name, "Edit" | "Write") {
72        if let Some(fp) = file_path {
73            let dedup_tag = format!("edit_after_read:{}", fp);
74            let already_exists = storage
75                .has_auto_insight(session_id, &dedup_tag)
76                .unwrap_or(true);
77            if !already_exists {
78                let was_read = storage
79                    .was_file_read_in_session(session_id, fp)
80                    .unwrap_or(false);
81                if was_read {
82                    insights.push(AutoInsight {
83                        content: format!(
84                            "File '{}' was read and then modified in this session, \
85                             indicating an informed change based on code review.",
86                            fp
87                        ),
88                        tags: vec![
89                            "auto-insight".to_string(),
90                            "edit-after-read".to_string(),
91                            format!(
92                                "file:{}",
93                                std::path::Path::new(fp)
94                                    .file_name()
95                                    .and_then(|f| f.to_str())
96                                    .unwrap_or("unknown")
97                            ),
98                        ],
99                        importance: 0.5,
100                        dedup_tag,
101                    });
102                }
103            }
104        }
105    }
106
107    // Trigger 3: "Understanding module" — 3+ files read from the same directory
108    // (reuses the directory focus data but generates a module-level insight)
109    if tool_name == "Read" {
110        if let Some(fp) = file_path {
111            let directory = std::path::Path::new(fp)
112                .parent()
113                .map(|p| p.to_string_lossy().to_string())
114                .unwrap_or_default();
115            if !directory.is_empty() {
116                let module_name = std::path::Path::new(&directory)
117                    .file_name()
118                    .and_then(|f| f.to_str())
119                    .unwrap_or("unknown");
120                let dedup_tag = format!("exploring_module:{}", directory);
121                let already_exists = storage
122                    .has_auto_insight(session_id, &dedup_tag)
123                    .unwrap_or(true);
124                if !already_exists {
125                    let count = storage
126                        .count_directory_reads(session_id, &directory)
127                        .unwrap_or(0);
128                    if count >= 3 {
129                        insights.push(AutoInsight {
130                            content: format!(
131                                "Exploring '{}' module: {} files read. Building understanding of this area.",
132                                module_name, count
133                            ),
134                            tags: vec![
135                                "auto-insight".to_string(),
136                                "exploring-module".to_string(),
137                                format!("module:{}", module_name),
138                            ],
139                            importance: 0.55,
140                            dedup_tag,
141                        });
142                    }
143                }
144            }
145        }
146    }
147
148    // Trigger 4: "Debugging" — Bash with error output followed by file reads
149    if tool_name == "Bash" {
150        let has_error = storage
151            .count_search_pattern_in_session(session_id, "error")
152            .unwrap_or(0)
153            > 0;
154        if has_error {
155            let area = file_path
156                .and_then(|fp| {
157                    std::path::Path::new(fp)
158                        .parent()
159                        .and_then(|p| p.file_name())
160                        .and_then(|f| f.to_str())
161                })
162                .unwrap_or("project");
163            let dedup_tag = format!("debugging:{}", area);
164            let already_exists = storage
165                .has_auto_insight(session_id, &dedup_tag)
166                .unwrap_or(true);
167            if !already_exists {
168                insights.push(AutoInsight {
169                    content: format!(
170                        "Debugging in '{}': error output detected in bash commands during this session.",
171                        area
172                    ),
173                    tags: vec![
174                        "auto-insight".to_string(),
175                        "debugging".to_string(),
176                        format!("area:{}", area),
177                    ],
178                    importance: 0.6,
179                    dedup_tag,
180                });
181            }
182        }
183    }
184
185    // Trigger 5: Same search pattern used 2+ times
186    if matches!(tool_name, "Grep" | "Glob") {
187        if let Some(pat) = pattern {
188            let dedup_tag = format!("repeated_search:{}", pat);
189            let already_exists = storage
190                .has_auto_insight(session_id, &dedup_tag)
191                .unwrap_or(true);
192            if !already_exists {
193                let count = storage
194                    .count_search_pattern_in_session(session_id, pat)
195                    .unwrap_or(0);
196                if count >= 2 {
197                    insights.push(AutoInsight {
198                        content: format!(
199                            "Search pattern '{}' used {} times in this session. \
200                             Consider storing a permanent memory for this recurring lookup.",
201                            pat, count
202                        ),
203                        tags: vec![
204                            "auto-insight".to_string(),
205                            "repeated-search".to_string(),
206                            format!("pattern:{}", pat),
207                        ],
208                        importance: 0.5,
209                        dedup_tag,
210                    });
211                }
212            }
213        }
214    }
215
216    insights
217}