use super::*;
const TAIL_WINDOW_BYTES: usize = 512 * 1024;
#[derive(Debug, Clone, Default)]
pub(crate) struct TailShape {
pub(crate) unreturned_use: Option<(String, Option<String>)>,
pub(crate) last_stop_reason: Option<String>,
pub(crate) last_ts_utc: Option<String>,
pub(crate) records_seen: usize,
}
pub(crate) fn tail_shape(path: &Path) -> Result<TailShape> {
let mut shape = TailShape::default();
let Some(mmap) = mmap_bytes(path)? else {
return Ok(shape);
};
let bytes: &[u8] = &mmap;
let start = bytes.len().saturating_sub(TAIL_WINDOW_BYTES);
let window = if start == 0 {
bytes
} else {
match memchr::memchr(b'\n', &bytes[start..]) {
Some(nl) => &bytes[start + nl + 1..],
None => &bytes[bytes.len()..],
}
};
let mut lines: Vec<&[u8]> = Vec::new();
let mut pos = 0usize;
while pos < window.len() {
match memchr::memchr(b'\n', &window[pos..]) {
Some(nl) => {
lines.push(&window[pos..pos + nl]);
pos += nl + 1;
}
None => break, }
}
let mut later_result_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
for line in lines.iter().rev() {
let Ok(Some(rec)) = crate::parse::parse_line(line) else {
continue;
};
shape.records_seen += 1;
if shape.last_ts_utc.is_none() {
shape.last_ts_utc = rec.timestamp.clone();
}
if shape.last_stop_reason.is_none() && rec.r#type.as_deref() == Some("assistant") {
if let Some(sr) = rec.message.as_ref().and_then(|m| m.stop_reason.as_deref()) {
shape.last_stop_reason = Some(sr.to_string());
}
}
if let Some(blocks) = rec.blocks() {
for b in blocks {
if let crate::model::Block::ToolResult {
tool_use_id: Some(id),
..
} = b
{
later_result_ids.insert(id.clone());
}
}
if shape.unreturned_use.is_none() {
for b in blocks {
if let crate::model::Block::ToolUse {
id: Some(id), name, ..
} = b
{
if !later_result_ids.contains(id) {
shape.unreturned_use = Some((
name.clone().unwrap_or_else(|| "(unnamed)".to_string()),
rec.timestamp.clone(),
));
}
}
}
}
}
if shape.unreturned_use.is_some()
&& shape.last_stop_reason.is_some()
&& shape.records_seen >= 8
{
break;
}
}
Ok(shape)
}
pub(crate) fn age_secs(ts_utc: Option<&str>) -> Option<i64> {
let t: jiff::Timestamp = ts_utc?.parse().ok()?;
Some((jiff::Timestamp::now().as_second() - t.as_second()).max(0))
}