use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
const TTL_SECS: u64 = 1800;
const MIN_INTERVAL_SECS: u64 = 300;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactDirective {
pub created_unix: u64,
pub keep_recent_turns: usize,
pub summary: String,
pub boundary_fingerprint: Option<String>,
pub original_handle: Option<String>,
}
fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn store_path() -> Option<std::path::PathBuf> {
crate::core::data_dir::lean_ctx_data_dir()
.ok()
.map(|d| d.join("compact_directive.json"))
}
pub fn create(keep_recent_turns: usize, summary: String) -> Result<(), String> {
if let Some(existing) = load_active()
&& now_unix().saturating_sub(existing.created_unix) < MIN_INTERVAL_SECS
{
return Err(format!(
"a compaction from {}s ago is still active — one compact per {}s \
(anti-obsession guard); use ctx_session action=\"restore\" first if needed",
now_unix().saturating_sub(existing.created_unix),
MIN_INTERVAL_SECS
));
}
let directive = CompactDirective {
created_unix: now_unix(),
keep_recent_turns,
summary,
boundary_fingerprint: None,
original_handle: None,
};
save(&directive)
}
pub fn save(directive: &CompactDirective) -> Result<(), String> {
let mut admitted = directive.clone();
admitted.summary =
crate::core::session::admission::admit_storable_tool_text(&directive.summary)?;
write_admitted(&admitted)
}
fn write_admitted(directive: &CompactDirective) -> Result<(), String> {
let path = store_path().ok_or_else(|| "no data dir".to_string())?;
let json = serde_json::to_string(directive).map_err(|e| e.to_string())?;
std::fs::write(path, json).map_err(|e| e.to_string())
}
pub fn load_active() -> Option<CompactDirective> {
let path = store_path()?;
let raw = std::fs::read_to_string(&path).ok()?;
let directive: CompactDirective = serde_json::from_str(&raw).ok()?;
if now_unix().saturating_sub(directive.created_unix) > TTL_SECS {
let _ = std::fs::remove_file(&path);
return None;
}
let original_summary = directive.summary.clone();
let Ok(admitted_summary) =
crate::core::session::admission::admit_storable_tool_text(&original_summary)
else {
let _ = std::fs::remove_file(path);
return None;
};
let mut admitted = directive;
admitted.summary = admitted_summary;
if admitted.summary != original_summary {
write_admitted(&admitted).ok()?;
}
Some(admitted)
}
pub fn clear() -> bool {
store_path().is_some_and(|path| std::fs::remove_file(path).is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn directive_roundtrip_rate_limit_and_clear() {
let _lock = crate::core::data_dir::test_env_lock();
let _dir = crate::core::data_dir::isolated_data_dir();
assert!(load_active().is_none());
create(6, "summary one".into()).expect("first create succeeds");
let d = load_active().expect("directive active");
assert_eq!(d.keep_recent_turns, 6);
assert!(d.boundary_fingerprint.is_none());
let err = create(4, "summary two".into()).expect_err("rate-limited");
assert!(err.contains("anti-obsession"), "{err}");
assert!(clear());
assert!(load_active().is_none());
create(4, "summary three".into()).expect("create after clear");
clear();
}
#[test]
fn expired_directive_is_ignored_and_removed() {
let _lock = crate::core::data_dir::test_env_lock();
let _dir = crate::core::data_dir::isolated_data_dir();
let stale = CompactDirective {
created_unix: now_unix() - TTL_SECS - 10,
keep_recent_turns: 6,
summary: "old".into(),
boundary_fingerprint: None,
original_handle: None,
};
save(&stale).unwrap();
assert!(load_active().is_none(), "expired directive must not apply");
assert!(load_active().is_none());
}
}