use super::*;
pub fn run_verbatim(args: &VerbatimArgs) -> Result<()> {
if !(args.round_trip_fraction > 0.0 && args.round_trip_fraction < 1.0) {
bail!(
"--round-trip-fraction must be in the open interval (0.0, 1.0), got {}",
args.round_trip_fraction
);
}
if args.slices.is_none() && args.budget == 0 {
bail!("--budget must be > 0");
}
if let Some(n) = args.slices {
if n == 0 {
bail!("--slices must be > 0 (it pins the fleet to N chunks)");
}
if args.slice.is_none() {
bail!("--slices N sets the fleet size; pass --slice i to pick which chunk to emit");
}
}
if let Some(slice) = args.slice {
if slice == 0 {
bail!("--slice is 1-based: the first chunk is --slice 1");
}
if args.window == 0 {
bail!("--window must be > 0");
}
if args.out.is_some() {
bail!(
"--slice and --out are mutually exclusive: --slice writes the selected chunk \
to stdout, --out writes the whole document to a file"
);
}
if matches!(args.format, OutputFormat::Json) {
bail!(
"--slice requires the text format (the chunked-injection use case is verbatim \
text); drop --format json"
);
}
}
let budget_chars = if let Some(n) = args.slices {
n.saturating_mul(args.window)
} else {
args.budget
};
let turn_range = args
.turn_range
.as_deref()
.map(parse_turn_range)
.transpose()?;
let time_window = TimeWindow::from_args(args.since.as_deref(), args.until.as_deref())?;
if args.paths.is_empty() && args.sessions_from.is_none() {
bail!(
"verbatim reconstructs ONE conversation's recent turns — name a target: `@<uuid>` / \
`@main` / `@<agent-id>` / a project path / `--sessions-from <FILE|->`. (A bare \
`csift verbatim` would realize --budget chars × EVERY session of EVERY project.)"
);
}
let session_files = path::resolve_targets_with_session_list(
&args.paths,
args.sessions_from.as_deref(),
args.want_subagents().into(),
path::Caller::Other,
)?;
let per_file: Vec<ScanResult> = session_files
.par_iter()
.map(|p| scan_one_file(p))
.collect::<Result<Vec<_>>>()?;
let mut skipped_lines = 0usize;
let mut sessions: Vec<ScanResult> = Vec::new();
for sr in per_file {
skipped_lines += sr.skipped_lines;
sessions.push(sr);
}
sessions.sort_by(|a, b| a.session_id.cmp(&b.session_id));
for sr in &mut sessions {
let bounds = turn_range.map(|spec| {
let tc = sr
.turns
.iter()
.map(|t| t.turn_index)
.max()
.map_or(0, |m| m + 1);
spec.resolve(tc, false)
});
sr.turns.retain(|t| {
let ts = t
.user
.as_ref()
.and_then(|u| u.ts_utc.as_deref())
.or_else(|| t.assistant_eot().and_then(|a| a.ts_utc.as_deref()));
window_admits(t.turn_index, ts, bounds, &time_window)
});
}
if args.slice.is_none() {
for sr in &sessions {
if sr.summaries.is_empty() {
eprintln!(
"csift: note: @{} has no compaction — nothing was clipped; for plain \
reading use `csift show @{} --turn <N|A..B|-k..>` (full records, no \
budget)",
sr.session_id, sr.session_id
);
}
}
}
let cfg = args.richness_cfg();
let plans: Vec<SessionPlan> = sessions
.iter()
.map(|sr| {
plan_session(
sr,
budget_chars,
args.round_trip_fraction,
args.max_compactions,
&cfg,
)
})
.collect();
let ctx = RenderCtx {
budget_chars,
rt_fraction: args.round_trip_fraction,
skipped_lines,
cfg,
};
match args.format {
OutputFormat::Text => render_text(
&ctx,
&sessions,
&plans,
args.out.as_deref(),
args.slice,
args.window,
args.slices,
)?,
OutputFormat::Json => render_json(&ctx, &sessions, &plans, args.out.as_deref())?,
}
Ok(())
}
pub(crate) fn window_admits(
turn_index: usize,
ts: Option<&str>,
turn_range: Option<(usize, usize)>,
time_window: &TimeWindow,
) -> bool {
if let Some((lo, hi)) = turn_range {
if turn_index < lo || turn_index > hi {
return false;
}
}
time_window.contains(ts)
}
pub(crate) fn scan_one_file(path: &Path) -> Result<ScanResult> {
let session_id = crate::subagent::session_id_from_path(path);
let is_subagent = crate::subagent::is_subagent_path(path);
let parent_session_id =
crate::subagent::parent_session_id_from_path(path).unwrap_or_else(|| session_id.clone());
let Some(mmap) = mmap_bytes(path)? else {
return Ok(ScanResult {
session_id,
is_subagent,
parent_session_id,
turns: Vec::new(),
summaries: Vec::new(),
skipped_lines: 0,
});
};
let bytes: &[u8] = &mmap;
let (records, mut skipped) = crate::parse::scan_lines_parallel(bytes, |line, line_no| {
if !line_is_turn_candidate(line) {
return crate::parse::non_candidate_verdict(line);
}
match crate::parse::parse_line(line) {
Ok(Some(rec)) => crate::parse::LineVerdict::Keep((line_no, rec)),
Ok(None) => crate::parse::LineVerdict::Ignore, Err(_) => crate::parse::LineVerdict::Skip, }
});
let mut sidecar: Vec<Record> = Vec::new();
if !is_subagent {
let (pending, pending_skipped) = crate::elicitation::unresolved_pending(path)?;
skipped += pending_skipped;
sidecar = pending;
}
let (turns, summaries) = build(&records, &sidecar);
Ok(ScanResult {
session_id,
is_subagent,
parent_session_id,
turns,
summaries,
skipped_lines: skipped,
})
}
pub(crate) fn line_is_turn_candidate(line: &[u8]) -> bool {
static TYPE_ASSISTANT: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(br#""type":"assistant""#));
static IS_COMPACT_SUMMARY: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"isCompactSummary"));
static TOOL_USE: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"tool_use"));
crate::parse::line_has_role_marker(line)
|| TYPE_ASSISTANT.find(line).is_some() || IS_COMPACT_SUMMARY.find(line).is_some() || TOOL_USE.find(line).is_some() }