use crate::state::AphroditeState;
pub fn on_session_start(state: &mut AphroditeState) -> serde_json::Value {
state.turn_counter = 0;
state.scanned_msg_idx = 0;
state.conv_index.clear();
state.recent_markers.clear();
state.referenced_files.clear();
state.tool_events.clear();
state.ephemeral_directives.clear();
state.manual_directive_turn = None;
state.last_emitted_marker_count = 0;
state.last_emitted_file_count = 0;
serde_json::json!({
"status": "ok",
"version": env!("CARGO_PKG_VERSION"),
"engine": "aphrodite-ffi",
"turn": 0,
"inline_entries": state.inline_store.len(),
})
}
pub fn next_turn(state: &mut AphroditeState) -> usize {
state.turn_counter += 1;
state.scanned_msg_idx = 0; state.turn_counter
}
pub fn archive_turn(state: &mut AphroditeState, hash: &str, summary: &str, size: usize) {
let turn = state.turn_counter;
state.conv_index.insert(turn, (hash.to_string(), summary.to_string(), size));
if state.conv_index.len() > 50 {
let oldest = state.conv_index.keys().min().copied();
if let Some(key) = oldest {
state.conv_index.remove(&key);
}
}
}
pub fn get_conv_index(state: &AphroditeState) -> Vec<serde_json::Value> {
let mut turns: Vec<_> = state.conv_index.iter().collect();
turns.sort_by_key(|(k, _)| *k);
turns
.iter()
.map(
|(turn, (hash, summary, size))| serde_json::json!({"turn": turn, "hash": hash, "summary": summary, "size": size}),
)
.collect()
}
pub fn catalog_summary(state: &mut AphroditeState) -> String {
let current_markers = state.recent_markers.len();
let prev_markers = state.last_emitted_marker_count;
state.last_emitted_marker_count = current_markers;
let mut parts = vec![];
let new_markers = current_markers.saturating_sub(prev_markers);
if new_markers > 0 && current_markers > 0 {
let previews: Vec<String> = state
.recent_markers
.iter()
.rev()
.take(new_markers)
.map(|m| m.preview.clone())
.collect();
parts.push(format!(
"[Aphrodite] +{} new compression{} this turn: {}. {} total.",
new_markers,
if new_markers == 1 { "" } else { "s" },
previews.join(", "),
current_markers,
));
} else if current_markers > 0 {
parts.push(format!("[Aphrodite] {} compressions (no change this turn).", current_markers));
}
let conv_count = state.conv_index.len();
if conv_count > 0 {
parts.push(format!("{} archived turns available for retrieval.", conv_count));
}
let current_files = state.referenced_files.len();
let prev_files = state.last_emitted_file_count;
state.last_emitted_file_count = current_files;
if current_files > 0 {
let new_files = current_files.saturating_sub(prev_files);
if new_files > 0 {
let names: Vec<String> = state
.referenced_files
.iter()
.rev()
.take(new_files)
.map(|(p, t)| format!("{} ({})", p, t))
.collect();
parts.push(format!(
"+{} new file{}: {}. {} total.",
new_files,
if new_files == 1 { "" } else { "s" },
names.join(", "),
current_files
));
} else {
parts.push(format!("{} referenced files (no change).", current_files));
}
}
if state.poll_worker_enabled {
let active: Vec<_> = state
.bg_tasks
.iter()
.filter(|t| t.status == crate::poll_worker::BgStatus::Running)
.collect();
if !active.is_empty() {
parts.push(format!("{} active poll worker(s). Use process(action='poll').", active.len()));
}
}
parts.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_start_resets() {
let mut s = AphroditeState::default();
s.turn_counter = 42;
s.record_marker(crate::state::MarkerEntry {
hash: "abc".into(),
ccr_type: "text".into(),
size: 100,
preview: "[text]".into(),
turn: 1,
center: None,
meta: None,
});
let r = on_session_start(&mut s);
assert_eq!(r["status"], "ok");
assert_eq!(s.turn_counter, 0);
assert_eq!(s.recent_markers.len(), 0);
}
#[test]
fn test_next_turn() {
let mut s = AphroditeState::default();
assert_eq!(next_turn(&mut s), 1);
assert_eq!(next_turn(&mut s), 2);
assert_eq!(s.scanned_msg_idx, 0);
}
#[test]
fn test_archive_eviction() {
let mut s = AphroditeState::default();
for i in 0..55 {
archive_turn(&mut s, &format!("hash{}", i), "summary", 100);
s.turn_counter += 1;
}
assert!(s.conv_index.len() <= 50);
assert!(!s.conv_index.contains_key(&1));
}
#[test]
fn test_get_conv_index_sorted_oldest_first() {
let mut s = AphroditeState::default();
s.turn_counter = 2;
archive_turn(&mut s, "hash2", "second", 20);
s.turn_counter = 1;
archive_turn(&mut s, "hash1", "first", 10);
let turns = get_conv_index(&s);
assert_eq!(turns.len(), 2);
assert_eq!(turns[0]["turn"], 1);
assert_eq!(turns[1]["turn"], 2);
}
#[test]
fn test_catalog_summary_delta_on_new_markers() {
let mut s = AphroditeState::default();
assert!(catalog_summary(&mut s).is_empty());
s.record_marker(crate::state::MarkerEntry {
hash: "abc".into(),
ccr_type: "text".into(),
size: 100,
preview: "[text] hello".into(),
turn: 1,
center: None,
meta: None,
});
assert!(catalog_summary(&mut s).contains("+1 new compression"));
assert!(catalog_summary(&mut s).contains("no change this turn"));
}
#[test]
fn test_catalog_summary_delta_on_new_files() {
let mut s = AphroditeState::default();
assert!(catalog_summary(&mut s).is_empty());
s.referenced_files.push_back(("src/a.rs".into(), "read_file".into()));
s.referenced_files.push_back(("src/b.rs".into(), "write_file".into()));
let r1 = catalog_summary(&mut s);
assert!(r1.contains("+2 new files"));
assert!(r1.contains("src/b.rs (write_file)"));
let r2 = catalog_summary(&mut s);
assert!(r2.contains("no change"));
}
#[test]
fn test_catalog_summary_reset_on_session_start() {
let mut s = AphroditeState::default();
s.last_emitted_marker_count = 10;
s.last_emitted_file_count = 5;
on_session_start(&mut s);
assert_eq!(s.last_emitted_marker_count, 0);
assert_eq!(s.last_emitted_file_count, 0);
}
}