1use crate::config::{
7 current_git_branch, default_actor, resolve_db_path, resolve_session_or_suggest,
8};
9use crate::error::{Error, Result};
10use crate::storage::SqliteStorage;
11use serde::Serialize;
12use std::path::PathBuf;
13
14const HIGH_PRIORITY_LIMIT: u32 = 50;
16const DECISION_LIMIT: u32 = 20;
17const REMINDER_LIMIT: u32 = 20;
18const PROGRESS_LIMIT: u32 = 10;
19
20#[derive(Serialize)]
22struct CompactionOutput {
23 checkpoint: CheckpointInfo,
24 stats: CompactionStats,
25 git_context: Option<GitContext>,
26 critical_context: CriticalContext,
27 restore_instructions: RestoreInstructions,
28}
29
30#[derive(Serialize)]
31struct CheckpointInfo {
32 id: String,
33 name: String,
34 session_id: String,
35 created_at: i64,
36}
37
38#[derive(Serialize)]
39struct CompactionStats {
40 total_items_saved: i64,
41 critical_items: usize,
42 pending_tasks: usize,
43 decisions_made: usize,
44}
45
46#[derive(Serialize)]
47struct GitContext {
48 branch: String,
49 files: Vec<String>,
50}
51
52#[derive(Serialize)]
53struct CriticalContext {
54 high_priority_items: Vec<ContextSummary>,
55 next_steps: Vec<ContextSummary>,
56 key_decisions: Vec<ContextSummary>,
57 recent_progress: Vec<ContextSummary>,
58}
59
60#[derive(Serialize)]
61struct ContextSummary {
62 key: String,
63 value: String,
64 category: String,
65 priority: String,
66}
67
68#[derive(Serialize)]
69struct RestoreInstructions {
70 tool: String,
71 checkpoint_id: String,
72 message: String,
73 summary: String,
74}
75
76pub fn execute(db_path: Option<&PathBuf>, actor: Option<&str>, session_id: Option<&str>, json: bool) -> Result<()> {
78 let db_path = resolve_db_path(db_path.map(|p| p.as_path())).ok_or(Error::NotInitialized)?;
79
80 if !db_path.exists() {
81 return Err(Error::NotInitialized);
82 }
83
84 let mut storage = SqliteStorage::open(&db_path)?;
85 let actor = actor.map(ToString::to_string).unwrap_or_else(default_actor);
86
87 let sid = resolve_session_or_suggest(session_id, &storage)?;
88 let session = storage
89 .get_session(&sid)?
90 .ok_or_else(|| Error::SessionNotFound { id: sid })?;
91
92 let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
94 let checkpoint_name = format!("pre-compact-{timestamp}");
95
96 let git_branch = current_git_branch();
98 let git_status = get_git_status();
99
100 let checkpoint_id = format!("ckpt_{}", &uuid::Uuid::new_v4().to_string()[..12]);
102
103 storage.create_checkpoint(
105 &checkpoint_id,
106 &session.id,
107 &checkpoint_name,
108 Some("Automatic checkpoint before context compaction"),
109 git_status.as_deref(),
110 git_branch.as_deref(),
111 &actor,
112 )?;
113
114 let all_items = storage.get_context_items(&session.id, None, None, Some(1000))?;
116 for item in &all_items {
117 storage.add_checkpoint_item(&checkpoint_id, &item.id, &actor)?;
118 }
119
120 let high_priority_items =
122 storage.get_context_items(&session.id, None, Some("high"), Some(HIGH_PRIORITY_LIMIT))?;
123
124 let reminders =
125 storage.get_context_items(&session.id, Some("reminder"), None, Some(REMINDER_LIMIT))?;
126
127 let decisions =
128 storage.get_context_items(&session.id, Some("decision"), None, Some(DECISION_LIMIT))?;
129
130 let progress =
131 storage.get_context_items(&session.id, Some("progress"), None, Some(PROGRESS_LIMIT))?;
132
133 let next_steps: Vec<_> = reminders
135 .iter()
136 .filter(|t| {
137 let lower = t.value.to_lowercase();
138 !lower.contains("completed")
139 && !lower.contains("done")
140 && !lower.contains("[completed]")
141 })
142 .take(5)
143 .collect();
144
145 let checkpoint = storage
147 .get_checkpoint(&checkpoint_id)?
148 .ok_or_else(|| Error::CheckpointNotFound {
149 id: checkpoint_id.clone(),
150 })?;
151
152 let git_files: Vec<String> = git_status
154 .as_ref()
155 .map(|s| {
156 s.lines()
157 .take(10)
158 .map(|line| line.trim().to_string())
159 .collect()
160 })
161 .unwrap_or_default();
162
163 if json {
164 let output = CompactionOutput {
165 checkpoint: CheckpointInfo {
166 id: checkpoint.id.clone(),
167 name: checkpoint.name.clone(),
168 session_id: session.id.clone(),
169 created_at: checkpoint.created_at,
170 },
171 stats: CompactionStats {
172 total_items_saved: checkpoint.item_count,
173 critical_items: high_priority_items.len(),
174 pending_tasks: next_steps.len(),
175 decisions_made: decisions.len(),
176 },
177 git_context: git_branch.as_ref().map(|branch| GitContext {
178 branch: branch.clone(),
179 files: git_files.clone(),
180 }),
181 critical_context: CriticalContext {
182 high_priority_items: high_priority_items
183 .iter()
184 .take(5)
185 .map(|i| ContextSummary {
186 key: i.key.clone(),
187 value: i.value.clone(),
188 category: i.category.clone(),
189 priority: i.priority.clone(),
190 })
191 .collect(),
192 next_steps: next_steps
193 .iter()
194 .map(|t| ContextSummary {
195 key: t.key.clone(),
196 value: t.value.clone(),
197 category: t.category.clone(),
198 priority: t.priority.clone(),
199 })
200 .collect(),
201 key_decisions: decisions
202 .iter()
203 .take(10)
204 .map(|d| ContextSummary {
205 key: d.key.clone(),
206 value: d.value.clone(),
207 category: d.category.clone(),
208 priority: d.priority.clone(),
209 })
210 .collect(),
211 recent_progress: progress
212 .iter()
213 .take(3)
214 .map(|p| ContextSummary {
215 key: p.key.clone(),
216 value: p.value.clone(),
217 category: p.category.clone(),
218 priority: p.priority.clone(),
219 })
220 .collect(),
221 },
222 restore_instructions: RestoreInstructions {
223 tool: "sc checkpoint restore".to_string(),
224 checkpoint_id: checkpoint.id.clone(),
225 message: format!(
226 "To continue this session, restore from checkpoint: {}",
227 checkpoint.name
228 ),
229 summary: format!(
230 "Session has {} pending tasks and {} key decisions recorded.",
231 next_steps.len(),
232 decisions.len()
233 ),
234 },
235 };
236 println!("{}", serde_json::to_string_pretty(&output)?);
237 } else {
238 println!("Context Compaction Prepared");
239 println!("===========================");
240 println!();
241 println!("Checkpoint: {}", checkpoint.name);
242 println!(" ID: {}", checkpoint.id);
243 println!(" Items saved: {}", checkpoint.item_count);
244 println!();
245
246 if let Some(ref branch) = git_branch {
247 println!("Git Context:");
248 println!(" Branch: {branch}");
249 if !git_files.is_empty() {
250 println!(" Changes:");
251 for file in git_files.iter().take(5) {
252 println!(" {file}");
253 }
254 }
255 println!();
256 }
257
258 println!("Critical Context:");
259 println!(
260 " High priority items: {}",
261 high_priority_items.len().min(5)
262 );
263 println!(" Pending tasks: {}", next_steps.len());
264 println!(" Key decisions: {}", decisions.len().min(10));
265 println!(" Recent progress: {}", progress.len().min(3));
266 println!();
267
268 if !next_steps.is_empty() {
269 println!("Next Steps:");
270 for step in next_steps.iter().take(3) {
271 println!(" - {} ({})", step.key, truncate(&step.value, 60));
272 }
273 println!();
274 }
275
276 if !decisions.is_empty() {
277 println!("Key Decisions:");
278 for decision in decisions.iter().take(3) {
279 println!(" - {} ({})", decision.key, truncate(&decision.value, 60));
280 }
281 println!();
282 }
283
284 println!("Restore Instructions:");
285 println!(" sc checkpoint restore {}", checkpoint.id);
286 }
287
288 Ok(())
289}
290
291fn get_git_status() -> Option<String> {
293 std::process::Command::new("git")
294 .args(["status", "--porcelain"])
295 .output()
296 .ok()
297 .filter(|output| output.status.success())
298 .map(|output| String::from_utf8_lossy(&output.stdout).to_string())
299}
300
301fn truncate(s: &str, max_len: usize) -> String {
303 if s.len() <= max_len {
304 s.to_string()
305 } else {
306 format!("{}...", &s[..max_len.saturating_sub(3)])
307 }
308}