use super::*;
pub(crate) const EXCERPT_MAX: usize = 400;
pub(crate) const REDACTED_THINKING_PLACEHOLDER: &str = "[redacted thinking]";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pairing {
Paired,
PendingNoResult,
OrphanResult,
}
#[derive(Debug, Clone, Default)]
pub struct ChainCounts {
pub abandoned_records: usize,
pub drafts: usize,
pub rewound_turns: usize,
pub replay_copies: usize,
pub boundary_cut_line: Option<usize>,
pub leaf_source: Option<&'static str>,
}
impl ChainCounts {
pub(crate) fn add(&mut self, other: &ChainCounts) {
let first = self.is_empty();
self.abandoned_records += other.abandoned_records;
self.drafts += other.drafts;
self.rewound_turns += other.rewound_turns;
self.replay_copies += other.replay_copies;
if first {
self.boundary_cut_line = other.boundary_cut_line;
self.leaf_source = other.leaf_source;
} else {
self.boundary_cut_line = None;
self.leaf_source = None;
}
}
fn is_empty(&self) -> bool {
self.boundary_cut_line.is_none() && self.leaf_source.is_none()
}
#[must_use]
pub fn any(&self) -> bool {
self.abandoned_records > 0 || self.replay_copies > 0 || self.boundary_cut_line.is_some()
}
}
#[derive(Debug, Clone)]
pub struct CompactionHit {
pub metadata: Option<serde_json::Value>,
pub mode: Option<crate::model::SummarizeMode>,
pub direction: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Hit {
pub class: Option<Class>,
pub labels: Vec<&'static str>,
pub excerpt: String,
pub timestamp_utc: Option<String>,
pub tool_name: Option<String>,
pub model: Option<String>,
pub attachment_type: Option<String>,
pub version: Option<String>,
pub is_error: Option<bool>,
pub direction: Option<(String, String)>,
pub tool_use_id: Option<String>,
pub pair: Option<Pairing>,
pub line: usize,
pub uuid: Option<String>,
pub raw: Option<String>,
pub image_ids: Vec<String>,
pub from_sidecar: bool,
pub queue_operation: Option<String>,
pub queue_reason: Option<String>,
pub delivery: Option<bool>,
pub resume_paired: Option<bool>,
pub survival: crate::model::Survival,
pub rewound_branch: bool,
pub replay_copy_of: Option<usize>,
pub compaction: Option<Box<CompactionHit>>,
pub truncated: bool,
}
impl Hit {
#[must_use]
pub fn delivered(&self) -> bool {
self.delivery
.unwrap_or_else(|| self.class.is_none_or(Class::llm_visible))
}
}
#[derive(Debug, Clone)]
pub struct DraftDiff {
pub superseding_line: usize,
pub superseding_uuid: Option<String>,
pub chars: usize,
pub pct: Option<f64>,
pub exact: bool,
}
impl DraftDiff {
pub(crate) fn text_line_for(&self, kind: Option<crate::model::Kind>) -> String {
if matches!(kind, Some(crate::model::Kind::Rewound { .. })) {
return format!(
"rewound: the conversation continued from L{} instead - {}",
self.superseding_line,
self.text_line()
);
}
self.text_line()
}
pub(crate) fn text_line(&self) -> String {
if self.exact && self.chars == 0 {
return "identical to the sent message".to_string();
}
let more = if self.exact { "" } else { "more than " };
match self.pct {
Some(p) => format!(
"differs from the sent message in {more}{} chars ({more}{p:.1}% of the final): \
may carry an addition or a correction",
self.chars
),
None => format!(
"differs from the sent message in {more}{} chars: may carry an addition or a \
correction",
self.chars
),
}
}
pub(crate) fn pct_json(&self) -> serde_json::Value {
match self.pct {
Some(p) => serde_json::json!((p * 10.0).round() / 10.0),
None => serde_json::Value::Null,
}
}
pub(crate) fn attach_json(&self, obj: &mut serde_json::Value) {
obj["superseding_line"] = serde_json::json!(self.superseding_line);
obj["superseding_uuid"] = serde_json::json!(self.superseding_uuid);
obj["diff_chars"] = serde_json::json!(self.chars);
obj["diff_pct"] = self.pct_json();
obj["diff_exact"] = serde_json::json!(self.exact);
}
}
#[derive(Debug, Clone)]
pub struct Exchange {
pub session_id: String,
pub is_subagent: bool,
pub parent_session_id: String,
pub turn_index: Option<usize>,
pub started_utc: Option<String>,
pub hits: Vec<Hit>,
pub siblings: Vec<Hit>,
pub siblings_hidden: usize,
pub turn_lines: (usize, usize),
pub record_uuids: Vec<String>,
pub superseded_draft: bool,
pub abandoned_kind: Option<crate::model::Kind>,
pub abandoned_root_line: Option<usize>,
pub draft_diff: Option<DraftDiff>,
}
#[derive(Debug, Clone, Default)]
pub struct SearchOutcome {
pub exchanges: Vec<Exchange>,
pub total_matched: usize,
pub total_sessions: usize,
pub dropped_by_cap: usize,
pub skipped_lines: usize,
pub chain: ChainCounts,
pub scope_top: usize,
pub scope_sub: usize,
}