pub const MAX_OPLOG_BATCHES: usize = 4096;
pub const DEFAULT_FALLBACK_WINDOW_HOURS: i64 = 1;
pub const EXCERPT_LEN: usize = 160;
pub const VERIFY_PASS_MIN_CONFIDENCE: f32 = 0.85;
pub fn duration_secs(since_ts: Option<i64>, until_ts: Option<i64>) -> Option<i64> {
match (since_ts, until_ts) {
(Some(lo), Some(hi)) => Some((hi - lo).max(0)),
_ => None,
}
}
pub fn window_overlaps(since_ts: Option<i64>, start: i64, end: Option<i64>) -> bool {
let Some(lo) = since_ts else {
return true;
};
match end {
None => true,
Some(e) => e >= lo || start >= lo,
}
}
pub fn agent_window_overlaps(
since_ts: Option<i64>,
is_active: bool,
last_activity_ts: i64,
) -> bool {
match since_ts {
Some(lo) => is_active || last_activity_ts >= lo,
None => true,
}
}
pub fn agent_task_window_overlaps(
since_ts: Option<i64>,
is_active_assigned: bool,
updated_at: i64,
completed_at: Option<i64>,
) -> bool {
match since_ts {
Some(lo) => {
is_active_assigned
|| updated_at >= lo
|| completed_at.is_some_and(|completed| completed >= lo)
}
None => true,
}
}
pub fn timeline_step_in_window(since_ms: Option<i64>, step_ms: Option<i64>) -> bool {
match since_ms {
None => true,
Some(lo) => step_ms.is_some_and(|ms| ms >= lo),
}
}
pub fn context_annotation_in_window(since_secs: Option<i64>, created_at: i64) -> bool {
match since_secs {
None => true,
Some(lo) => created_at >= lo,
}
}
pub fn choose_default_since_ts(
now_secs: i64,
recent_turn_ts: Option<i64>,
has_head: bool,
fallback_hours: i64,
) -> Option<i64> {
let one_window_ago = now_secs.saturating_sub(fallback_hours.saturating_mul(3600));
match (recent_turn_ts, has_head) {
(Some(turn_ts), _) if turn_ts > one_window_ago => Some(turn_ts),
(_, true) => Some(one_window_ago),
_ => None,
}
}
pub fn is_turn_boundary_intent(intent: &str) -> bool {
intent.contains("Claude Code turn")
|| intent.contains("session segment")
|| intent.contains("UserPromptSubmit")
}
pub fn is_verify_pass_signal(intent: &str, confidence: Option<f32>) -> bool {
intent.starts_with("verified:") && confidence.unwrap_or(0.0) >= VERIFY_PASS_MIN_CONFIDENCE
}
pub fn is_verify_fail_marker(name: &str) -> bool {
name.starts_with("failed-")
}
pub fn excerpt(s: &str) -> String {
let trimmed = s.trim();
if trimmed.chars().count() <= EXCERPT_LEN {
return trimmed.to_string();
}
let take: String = trimmed.chars().take(EXCERPT_LEN).collect();
format!("{take}…")
}
pub fn display_free_text(s: &str, verbose: bool) -> String {
if verbose {
s.trim().to_string()
} else {
scrub_path_like_tokens(&excerpt(s))
}
}
pub fn scrub_path_like_tokens(s: &str) -> String {
s.split_whitespace()
.map(|token| {
if is_path_like_token(token) {
"[redacted-path]"
} else {
token
}
})
.collect::<Vec<_>>()
.join(" ")
}
pub fn is_path_like_token(token: &str) -> bool {
let trimmed = token.trim_matches(|c: char| {
matches!(
c,
'"' | '\''
| '`'
| ','
| ';'
| ':'
| '.'
| '!'
| '?'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
)
});
if trimmed.contains('/') || trimmed.contains('\\') {
return true;
}
let Some((stem, extension)) = trimmed.rsplit_once('.') else {
return false;
};
!stem.is_empty()
&& !extension.is_empty()
&& extension.len() <= 10
&& extension.chars().all(|c| c.is_ascii_alphanumeric())
&& stem
.chars()
.any(|c| c.is_ascii_alphabetic() || matches!(c, '-' | '_'))
}
pub fn short_state_id(id: &str) -> &str {
let id_no_prefix = id.strip_prefix("hs-").unwrap_or(id);
let prefix_len = if id.starts_with("hs-") { 3 } else { 0 };
let take = std::cmp::min(8, id_no_prefix.len());
&id[..(prefix_len + take)]
}
pub fn format_duration_label(duration_secs: Option<i64>) -> String {
duration_secs
.map(|s| format!("{s}s"))
.unwrap_or_else(|| "<unknown>".to_string())
}
pub fn default_since_label() -> &'static str {
"<default-window>"
}
pub fn no_head_until_label() -> &'static str {
"<no-head>"
}
pub fn retro_header_line(
since: Option<&str>,
until: Option<&str>,
duration_secs: Option<i64>,
) -> String {
format!(
"Retro: since={} until={} duration={}",
since.unwrap_or(default_since_label()),
until.unwrap_or(no_head_until_label()),
format_duration_label(duration_secs),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn duration_and_window_overlap() {
assert_eq!(duration_secs(Some(100), Some(150)), Some(50));
assert_eq!(duration_secs(Some(150), Some(100)), Some(0));
assert_eq!(duration_secs(None, Some(100)), None);
assert_eq!(duration_secs(Some(100), None), None);
assert!(window_overlaps(None, 0, Some(1)));
assert!(window_overlaps(Some(100), 50, None));
assert!(window_overlaps(Some(100), 50, Some(120)));
assert!(window_overlaps(Some(100), 110, Some(120)));
assert!(!window_overlaps(Some(100), 10, Some(20)));
}
#[test]
fn agent_and_task_predicates() {
assert!(agent_window_overlaps(None, false, 0));
assert!(agent_window_overlaps(Some(100), true, 0));
assert!(agent_window_overlaps(Some(100), false, 100));
assert!(!agent_window_overlaps(Some(100), false, 50));
assert!(agent_task_window_overlaps(None, false, 0, None));
assert!(agent_task_window_overlaps(Some(100), true, 0, None));
assert!(agent_task_window_overlaps(Some(100), false, 100, None));
assert!(agent_task_window_overlaps(Some(100), false, 0, Some(150)));
assert!(!agent_task_window_overlaps(Some(100), false, 10, Some(20)));
assert!(timeline_step_in_window(None, None));
assert!(!timeline_step_in_window(Some(10), None));
assert!(timeline_step_in_window(Some(10), Some(10)));
assert!(context_annotation_in_window(None, 0));
assert!(!context_annotation_in_window(Some(10), 5));
}
#[test]
fn default_since_prefers_recent_turn() {
let now = 1_000_000;
assert_eq!(
choose_default_since_ts(now, Some(now - 60), true, DEFAULT_FALLBACK_WINDOW_HOURS),
Some(now - 60)
);
assert_eq!(
choose_default_since_ts(now, Some(now - 10_000), true, DEFAULT_FALLBACK_WINDOW_HOURS),
Some(now - 3600)
);
assert_eq!(
choose_default_since_ts(now, None, false, DEFAULT_FALLBACK_WINDOW_HOURS),
None
);
}
#[test]
fn verify_and_turn_heuristics() {
assert!(is_verify_pass_signal("verified: ok", Some(0.9)));
assert!(!is_verify_pass_signal("verified: ok", Some(0.5)));
assert!(!is_verify_pass_signal("other", Some(0.9)));
assert!(is_verify_fail_marker("failed-123"));
assert!(!is_verify_fail_marker("v1.0.0"));
assert!(is_turn_boundary_intent("Claude Code turn start"));
assert!(is_turn_boundary_intent("session segment boundary"));
assert!(is_turn_boundary_intent("UserPromptSubmit hook"));
assert!(!is_turn_boundary_intent("regular capture"));
}
#[test]
fn excerpt_and_scrub() {
let long = "a".repeat(EXCERPT_LEN + 50);
let out = excerpt(&long);
assert_eq!(out.chars().count(), EXCERPT_LEN + 1);
assert!(out.ends_with('…'));
assert_eq!(excerpt("short content"), "short content");
let s =
"Review src/lib.rs and private-secret-name.txt. Check secret.env! Maybe docs/plan.md?";
let scrubbed = display_free_text(s, false);
assert!(!scrubbed.contains("src/lib.rs"));
assert!(!scrubbed.contains("private-secret-name.txt"));
assert!(scrubbed.contains("[redacted-path]"));
assert_eq!(display_free_text(s, true), s);
}
#[test]
fn short_id_and_header() {
assert_eq!(short_state_id("hs-abcdef0123456789"), "hs-abcdef01");
assert_eq!(short_state_id("short"), "short");
let line = retro_header_line(None, None, Some(42));
assert!(line.contains("since=<default-window>"));
assert!(line.contains("until=<no-head>"));
assert!(line.contains("duration=42s"));
}
}