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 (buf, start) = read_tail(path, TAIL_WINDOW_BYTES as u64)?;
if buf.is_empty() {
return Ok(shape);
}
let bytes: &[u8] = &buf;
let window = if start == 0 {
bytes
} else {
match memchr::memchr(b'\n', bytes) {
Some(nl) => &bytes[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) const CHECKPOINT_KIND: &str = "cost-state";
#[derive(Debug, Clone)]
pub(crate) struct CheckpointTail {
pub(crate) line: usize,
pub(crate) kind: &'static str,
}
pub(crate) fn last_checkpoint(path: &Path) -> Result<Option<CheckpointTail>> {
static COST: std::sync::LazyLock<memchr::memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b"\"cost-state\""));
let (buf, start) = read_tail(path, TAIL_WINDOW_BYTES as u64)?;
let mut end = buf.len();
while end > 0 && buf[end - 1].is_ascii_whitespace() {
end -= 1;
}
if end == 0 {
return Ok(None);
}
let line_start = memchr::memrchr(b'\n', &buf[..end]).map_or(0, |i| i + 1);
if line_start == 0 && start > 0 {
return Ok(None);
}
let line = &buf[line_start..end];
if COST.find(line).is_none() {
return Ok(None);
}
let Ok(Some(rec)) = crate::parse::parse_line(line) else {
return Ok(None);
};
if rec.r#type.as_deref() != Some(CHECKPOINT_KIND) {
return Ok(None);
}
let at = start + line_start as u64;
Ok(Some(CheckpointTail {
line: count_newlines_before(path, at)? + 1,
kind: CHECKPOINT_KIND,
}))
}
fn count_newlines_before(path: &Path, offset: u64) -> Result<usize> {
const CHUNK: u64 = 4 * 1024 * 1024;
let mut n = 0usize;
let mut at = 0u64;
while at < offset {
let buf = read_range(path, at, (at + CHUNK).min(offset))?;
if buf.is_empty() {
break; }
n += memchr::memchr_iter(b'\n', &buf).count();
at += buf.len() as u64;
}
Ok(n)
}
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))
}