diff --git a/crates/relayburn-cli/src/commands/compare.rs b/crates/relayburn-cli/src/commands/compare.rs index e8dae66c..fed40973 100644 --- a/crates/relayburn-cli/src/commands/compare.rs +++ b/crates/relayburn-cli/src/commands/compare.rs @@ -116,9 +116,7 @@ fn run_inner(globals: &GlobalArgs, args: CompareArgs) -> Result { if args.include_partial { if let Some(raw) = args.fidelity.as_deref() { if raw != "partial" { - return Err(anyhow!( - "--include-partial conflicts with --fidelity {raw}" - )); + return Err(anyhow!("--include-partial conflicts with --fidelity {raw}")); } } min_fidelity = FidelityClass::Partial; @@ -128,7 +126,9 @@ fn run_inner(globals: &GlobalArgs, args: CompareArgs) -> Result { // per-command so the global JSON take-precedence rule in the TS CLI // becomes "explicit conflict" here — same exit code, same message. if globals.json && args.csv { - return Err(anyhow!("--json and --csv are mutually exclusive; pick one.")); + return Err(anyhow!( + "--json and --csv are mutually exclusive; pick one." + )); } // 4. Provider filter. Lower-cased CSV; turns whose effective provider @@ -238,12 +238,7 @@ fn run_inner(globals: &GlobalArgs, args: CompareArgs) -> Result { print!("{csv}"); return Ok(0); } - let tty = render_tty( - &table, - analyzed_turns, - min_fidelity, - &fidelity_summary, - ); + let tty = render_tty(&table, analyzed_turns, min_fidelity, &fidelity_summary); print!("{tty}"); Ok(0) } @@ -490,13 +485,24 @@ fn build_json( /// `preserve_order` feature). fn fidelity_summary_to_value(s: &FidelitySummary) -> Value { let mut by_class = serde_json::Map::new(); - for key in &["full", "usage-only", "aggregate-only", "cost-only", "partial"] { + for key in &[ + "full", + "usage-only", + "aggregate-only", + "cost-only", + "partial", + ] { let cls = parse_fidelity(key).unwrap(); let n = s.by_class.get(&cls).copied().unwrap_or(0); by_class.insert((*key).to_string(), Value::from(n)); } let mut by_granularity = serde_json::Map::new(); - for key in &["per-turn", "per-message", "per-session-aggregate", "cost-only"] { + for key in &[ + "per-turn", + "per-message", + "per-session-aggregate", + "cost-only", + ] { let g = match *key { "per-turn" => UsageGranularity::PerTurn, "per-message" => UsageGranularity::PerMessage, @@ -589,15 +595,9 @@ fn render_csv(table: &CompareTable) -> String { c.one_shot_turns.to_string(), c.priced_turns.to_string(), num_csv(c.total_cost, 6), - c.cost_per_turn - .map(|v| num_csv(v, 6)) - .unwrap_or_default(), - c.one_shot_rate - .map(|v| num_csv(v, 4)) - .unwrap_or_default(), - c.cache_hit_rate - .map(|v| num_csv(v, 4)) - .unwrap_or_default(), + c.cost_per_turn.map(|v| num_csv(v, 6)).unwrap_or_default(), + c.one_shot_rate.map(|v| num_csv(v, 4)).unwrap_or_default(), + c.cache_hit_rate.map(|v| num_csv(v, 4)).unwrap_or_default(), c.median_retries .map(|v| { // `String(n)` for numbers; JS prints integers as-is. @@ -662,7 +662,10 @@ fn render_tty( ) -> String { let mut lines: Vec = Vec::new(); lines.push(String::new()); - lines.push(format!("turns analyzed: {}", format_uint(analyzed_turns as u64))); + lines.push(format!( + "turns analyzed: {}", + format_uint(analyzed_turns as u64) + )); let excluded = compute_excluded(summary, minimum); if excluded.total > 0 { @@ -671,9 +674,8 @@ fn render_tty( lines.push(String::new()); if table.models.is_empty() || table.categories.is_empty() { - lines.push( - "no data to compare (need turns spanning ≥1 model and ≥1 activity).".to_string(), - ); + lines + .push("no data to compare (need turns spanning ≥1 model and ≥1 activity).".to_string()); lines.push(String::new()); return lines.join("\n"); } @@ -751,10 +753,7 @@ fn render_tty( // Coverage notes. let mut notes: Vec = Vec::new(); for cat in &table.categories { - let any_has_data = table - .models - .iter() - .any(|m| !cell_for(m, cat).no_data); + let any_has_data = table.models.iter().any(|m| !cell_for(m, cat).no_data); if !any_has_data { continue; } @@ -931,7 +930,10 @@ mod tests { #[test] fn parse_fidelity_known_classes() { - assert!(matches!(parse_fidelity("full").unwrap(), FidelityClass::Full)); + assert!(matches!( + parse_fidelity("full").unwrap(), + FidelityClass::Full + )); assert!(matches!( parse_fidelity("usage-only").unwrap(), FidelityClass::UsageOnly @@ -941,7 +943,10 @@ mod tests { #[test] fn display_model_name_strips_provider_prefix() { - assert_eq!(display_model_name("anthropic/claude-sonnet-4-6"), "claude-sonnet-4-6"); + assert_eq!( + display_model_name("anthropic/claude-sonnet-4-6"), + "claude-sonnet-4-6" + ); assert_eq!(display_model_name("claude-haiku-4-5"), "claude-haiku-4-5"); } } diff --git a/crates/relayburn-cli/src/commands/flow.rs b/crates/relayburn-cli/src/commands/flow.rs index 3cbcae2a..75b8c7ef 100644 --- a/crates/relayburn-cli/src/commands/flow.rs +++ b/crates/relayburn-cli/src/commands/flow.rs @@ -194,7 +194,12 @@ fn render_mermaid(graph: &FlowGraph) -> String { for node in &graph.nodes { let safe_id = mermaid_id(&node.id); let label = mermaid_label(&node.label, node.turn_number); - writeln!(out, " {safe_id}[\"{label}\"]:::{}", mermaid_class(node.kind)).unwrap(); + writeln!( + out, + " {safe_id}[\"{label}\"]:::{}", + mermaid_class(node.kind) + ) + .unwrap(); } if !graph.edges.is_empty() { @@ -277,11 +282,7 @@ fn render_svg(graph: &FlowGraph) -> String { let height = max_y + LEGEND_HEIGHT + SVG_MARGIN * 2; let mut out = String::with_capacity(4096); - writeln!( - out, - "" - ) - .unwrap(); + writeln!(out, "").unwrap(); writeln!( out, "" @@ -518,7 +519,10 @@ const _ASSERT_LAYOUT_CONSTANTS: () = { #[cfg(test)] mod tests { use super::*; - use relayburn_sdk::{flow_graph_from_trees, FlowOpts as SdkFlowOpts, SpanAttrValue, SpanKind, SpanNode, TurnSpanTree}; + use relayburn_sdk::{ + flow_graph_from_trees, FlowOpts as SdkFlowOpts, SpanAttrValue, SpanKind, SpanNode, + TurnSpanTree, + }; fn turn_root() -> SpanNode { SpanNode::new(SpanKind::Turn, "turn") diff --git a/crates/relayburn-cli/src/commands/hotspots.rs b/crates/relayburn-cli/src/commands/hotspots.rs index 2f23767b..2219ec8b 100644 --- a/crates/relayburn-cli/src/commands/hotspots.rs +++ b/crates/relayburn-cli/src/commands/hotspots.rs @@ -141,7 +141,7 @@ fn resolve_pattern_selection(raw: &str) -> Result, String> { } let mut out: Vec = Vec::new(); for piece in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { - if !PATTERN_KINDS.iter().any(|k| *k == piece) { + if !PATTERN_KINDS.contains(&piece) { return Err(format!( "unknown --patterns value \"{}\". Valid: {}", piece, @@ -1156,10 +1156,7 @@ fn format_bytes(bytes: u64) -> String { // ---- sort helpers --------------------------------------------------------- -fn sort_file<'a>( - rows: &'a [FileAggregation], - rank_by: RankBy, -) -> (&'static str, Vec<&'a FileAggregation>) { +fn sort_file(rows: &[FileAggregation], rank_by: RankBy) -> (&'static str, Vec<&FileAggregation>) { let mut out: Vec<&FileAggregation> = rows.iter().collect(); match rank_by { RankBy::Cost => ( @@ -1168,49 +1165,46 @@ fn sort_file<'a>( out, ), RankBy::Bytes => { - out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes)); + out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes)); ("Top files by output bytes", out) } } } -fn sort_bash<'a>( - rows: &'a [BashAggregation], - rank_by: RankBy, -) -> (&'static str, Vec<&'a BashAggregation>) { +fn sort_bash(rows: &[BashAggregation], rank_by: RankBy) -> (&'static str, Vec<&BashAggregation>) { let mut out: Vec<&BashAggregation> = rows.iter().collect(); match rank_by { RankBy::Cost => ("Top exact Bash commands by cost", out), RankBy::Bytes => { - out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes)); + out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes)); ("Top exact Bash commands by output bytes", out) } } } -fn sort_bash_verb<'a>( - rows: &'a [BashVerbAggregation], +fn sort_bash_verb( + rows: &[BashVerbAggregation], rank_by: RankBy, -) -> (&'static str, Vec<&'a BashVerbAggregation>) { +) -> (&'static str, Vec<&BashVerbAggregation>) { let mut out: Vec<&BashVerbAggregation> = rows.iter().collect(); match rank_by { RankBy::Cost => ("Top Bash verbs by cost", out), RankBy::Bytes => { - out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes)); + out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes)); ("Top Bash verbs by output bytes", out) } } } -fn sort_subagent<'a>( - rows: &'a [SubagentAggregation], +fn sort_subagent( + rows: &[SubagentAggregation], rank_by: RankBy, -) -> (&'static str, Vec<&'a SubagentAggregation>) { +) -> (&'static str, Vec<&SubagentAggregation>) { let mut out: Vec<&SubagentAggregation> = rows.iter().collect(); match rank_by { RankBy::Cost => ("Top subagent calls by cost", out), RankBy::Bytes => { - out.sort_by(|a, b| b.total_output_bytes.cmp(&a.total_output_bytes)); + out.sort_by_key(|b| std::cmp::Reverse(b.total_output_bytes)); ("Top subagent calls by output bytes", out) } } diff --git a/crates/relayburn-cli/src/commands/ingest.rs b/crates/relayburn-cli/src/commands/ingest.rs index 53ef8cbc..70d77518 100644 --- a/crates/relayburn-cli/src/commands/ingest.rs +++ b/crates/relayburn-cli/src/commands/ingest.rs @@ -247,12 +247,13 @@ fn run_watch(globals: &GlobalArgs, args: &IngestArgs) -> i32 { }); let progress_for_error = progress_for_loop.clone(); - let on_error: relayburn_sdk::ErrorSink = Arc::new(move |err: &anyhow::Error| match &progress_for_error { - Some(p) => p.suspend(|| { - eprintln!("[burn] ingest: {err}"); - }), - None => eprintln!("[burn] ingest: {err}"), - }); + let on_error: relayburn_sdk::ErrorSink = + Arc::new(move |err: &anyhow::Error| match &progress_for_error { + Some(p) => p.suspend(|| { + eprintln!("[burn] ingest: {err}"); + }), + None => eprintln!("[burn] ingest: {err}"), + }); // Default to the `notify`-backed FS-event driver against the // three session-store roots ingest scans. Falls back to polling diff --git a/crates/relayburn-cli/src/commands/mcp_server.rs b/crates/relayburn-cli/src/commands/mcp_server.rs index ecaf7c29..6332a8fd 100644 --- a/crates/relayburn-cli/src/commands/mcp_server.rs +++ b/crates/relayburn-cli/src/commands/mcp_server.rs @@ -176,7 +176,12 @@ impl Server { } }; if !value.is_object() { - write_response(&error_envelope(&Value::Null, -32600, "invalid request", None)); + write_response(&error_envelope( + &Value::Null, + -32600, + "invalid request", + None, + )); return; } @@ -421,11 +426,12 @@ impl Server { // Mirror TS: when no override and no registered default, surface // a more descriptive note than the SDK's generic one. - if payload.session_id.is_none() && override_id.is_none() && self.default_session_id.is_none() + if payload.session_id.is_none() + && override_id.is_none() + && self.default_session_id.is_none() { - payload.note = Some( - "no session id provided and server was not registered with one".to_string(), - ); + payload.note = + Some("no session id provided and server was not registered with one".to_string()); } let value = serde_json::to_value(&payload).unwrap_or(Value::Null); diff --git a/crates/relayburn-cli/src/commands/overhead.rs b/crates/relayburn-cli/src/commands/overhead.rs index e36e2369..04e08b6a 100644 --- a/crates/relayburn-cli/src/commands/overhead.rs +++ b/crates/relayburn-cli/src/commands/overhead.rs @@ -368,11 +368,7 @@ fn format_line_range(start: u64, end: u64) -> String { // `burn overhead deltas` (#432) // --------------------------------------------------------------------------- -fn run_deltas( - globals: &GlobalArgs, - since: Option, - args: OverheadDeltasArgs, -) -> i32 { +fn run_deltas(globals: &GlobalArgs, since: Option, args: OverheadDeltasArgs) -> i32 { let opts = ContextDeltaOpts { session: args.session.clone(), since: since.as_deref().and_then(parse_since_duration), @@ -626,9 +622,7 @@ mod tests { approx_bytes: 400, truncated: false, }, - InterveningStep::Compaction { - tokens_freed: 5000, - }, + InterveningStep::Compaction { tokens_freed: 5000 }, ]; let s = driver_summary(&steps); assert!(s.contains("compaction")); diff --git a/crates/relayburn-cli/src/commands/state.rs b/crates/relayburn-cli/src/commands/state.rs index a68101f0..9c4974a3 100644 --- a/crates/relayburn-cli/src/commands/state.rs +++ b/crates/relayburn-cli/src/commands/state.rs @@ -77,10 +77,7 @@ fn format_status(s: &StateStatus) -> String { let mut out = String::new(); out.push_str(&format!("derived state at {}:\n", s.home)); out.push_str("events DB (burn.sqlite):\n"); - out.push_str(&format!( - " path: {}\n", - rel_to_home(&s.burn.path, &s.home) - )); + out.push_str(&format!(" path: {}\n", rel_to_home(&s.burn.path, &s.home))); if !s.burn.exists { out.push_str(" status: not built yet\n"); } @@ -315,8 +312,7 @@ fn run_prune(globals: &GlobalArgs, args: crate::cli::StatePruneArgs) -> i32 { Retention::Forever => { progress.finish_and_clear(); if globals.json { - let payload = - serde_json::json!({ "rowsDeleted": 0, "bytesFreed": 0, "retention": "forever" }); + let payload = serde_json::json!({ "rowsDeleted": 0, "bytesFreed": 0, "retention": "forever" }); let _ = render_json(&payload); } else { println!("content retention=forever - nothing to prune"); @@ -492,7 +488,12 @@ fn run_reset(globals: &GlobalArgs, args: crate::cli::StateResetArgs) -> i32 { }; progress.finish_and_clear(); - print_reset_report(globals, &summary, /*executed=*/ true, ingest_report.as_ref()) + print_reset_report( + globals, + &summary, + /*executed=*/ true, + ingest_report.as_ref(), + ) } /// Drive a single `ingest_all` sweep on the open handle. @@ -548,14 +549,22 @@ fn print_reset_report( format_uint(summary.stamps_dropped as u64), if summary.stamps_dropped == 1 { "" } else { "s" }, format_uint(summary.content_rows_dropped as u64), - if summary.content_rows_dropped == 1 { "" } else { "s" }, + if summary.content_rows_dropped == 1 { + "" + } else { + "s" + }, ); match ingest_report { Some(report) => { println!( " re-ingested {} session{} (+{} turn{}).", format_uint(report.ingested_sessions as u64), - if report.ingested_sessions == 1 { "" } else { "s" }, + if report.ingested_sessions == 1 { + "" + } else { + "s" + }, format_uint(report.appended_turns as u64), if report.appended_turns == 1 { "" } else { "s" }, ); @@ -722,10 +731,7 @@ mod tests { rel_to_home("/x/home/burn.sqlite", "/x/home/"), "${RELAYBURN_HOME}/burn.sqlite" ); - assert_eq!( - rel_to_home("/x/home2/foo", "/x/home/"), - "/x/home2/foo" - ); + assert_eq!(rel_to_home("/x/home2/foo", "/x/home/"), "/x/home2/foo"); } #[test] diff --git a/crates/relayburn-cli/src/commands/summary.rs b/crates/relayburn-cli/src/commands/summary.rs index 830a8334..94b66392 100644 --- a/crates/relayburn-cli/src/commands/summary.rs +++ b/crates/relayburn-cli/src/commands/summary.rs @@ -216,15 +216,13 @@ fn run_inner(globals: &GlobalArgs, args: SummaryArgs) -> anyhow::Result { None => LedgerOpenOptions::default(), }; progress.set_task("opening ledger"); - let mut handle = Ledger::open(opts).map_err(|err| { + let mut handle = Ledger::open(opts).inspect_err(|_| { progress.finish_and_clear(); - err })?; - let ingest_report = - run_ingest(&mut handle, &progress, globals.ledger_path.clone()).map_err(|err| { + let ingest_report = run_ingest(&mut handle, &progress, globals.ledger_path.clone()) + .inspect_err(|_| { progress.finish_and_clear(); - err })?; let mode = if let Some(session_id) = subagent_tree_session_id { @@ -244,27 +242,27 @@ fn run_inner(globals: &GlobalArgs, args: SummaryArgs) -> anyhow::Result { }; progress.set_task("building summary"); - let report = handle.summary_report(SummaryReportOptions { - session: args.session, - project: args.project, - since: args.since, - workflow: args.workflow, - tags: if tag_filter.is_empty() { - None - } else { - Some(tag_filter) - }, - group_by_tag: args.group_by_tag, - agent: args.agent, - providers: provider_filter.map(|providers| providers.into_iter().collect()), - mode, - include_quality: args.quality, - ledger_home: None, - }) - .map_err(|err| { - progress.finish_and_clear(); - err - })?; + let report = handle + .summary_report(SummaryReportOptions { + session: args.session, + project: args.project, + since: args.since, + workflow: args.workflow, + tags: if tag_filter.is_empty() { + None + } else { + Some(tag_filter) + }, + group_by_tag: args.group_by_tag, + agent: args.agent, + providers: provider_filter.map(|providers| providers.into_iter().collect()), + mode, + include_quality: args.quality, + ledger_home: None, + }) + .inspect_err(|_| { + progress.finish_and_clear(); + })?; progress.finish_and_clear(); match report { @@ -333,9 +331,8 @@ fn run_ingest( ) -> anyhow::Result { progress.set_task("refreshing ledger"); let opts = progress.ingest_options(ledger_home); - ingest_all(handle.raw_mut(), &opts).map_err(|err| { + ingest_all(handle.raw_mut(), &opts).inspect_err(|_| { progress.finish_and_clear(); - err }) } @@ -490,7 +487,10 @@ fn grouped_json_value( summary_replacement_savings_to_value(&report.replacement_savings), ); } - payload.insert("stopReasons".into(), stop_reasons_to_json(&report.stop_reasons)); + payload.insert( + "stopReasons".into(), + stop_reasons_to_json(&report.stop_reasons), + ); if !report.subagents.is_empty() { // `subagents: {paired, orphan, total}` (issue #435). Skipped // when both buckets are zero so the JSON shape stays compact @@ -849,7 +849,7 @@ fn render_subagent_tree_report( if root.cumulative_turns == 1 { "" } else { "s" }, )); out.push(String::new()); - out.extend(render_tree(&root)); + out.extend(render_tree(root)); out.push(String::new()); print!("{}", out.join("\n")); Ok(0) @@ -1289,7 +1289,10 @@ mod tests { // counts. Issue #435 explicitly wants both numbers even when // one is zero, so a slash-command-only session showing only // orphans is still scannable. - let counts = SubagentCounts { paired: 2, orphan: 1 }; + let counts = SubagentCounts { + paired: 2, + orphan: 1, + }; assert_eq!( format_subagents_line(&counts), "subagents: 2 paired, 1 orphan" @@ -1328,7 +1331,10 @@ mod tests { "subagents key must be omitted when counts are zero; got {value}" ); - report.subagents = SubagentCounts { paired: 2, orphan: 1 }; + report.subagents = SubagentCounts { + paired: 2, + orphan: 1, + }; let value = grouped_json_value(&report, &relayburn_sdk::IngestReport::empty()); assert_eq!( value["subagents"], diff --git a/crates/relayburn-cli/src/harnesses/claude.rs b/crates/relayburn-cli/src/harnesses/claude.rs index eaabbd03..896a36d9 100644 --- a/crates/relayburn-cli/src/harnesses/claude.rs +++ b/crates/relayburn-cli/src/harnesses/claude.rs @@ -141,7 +141,10 @@ mod tests { assert_eq!(plan.args[0], "--session-id"); let sid = plan.args.get(1).cloned().unwrap_or_default(); assert!(plan.session_id.as_deref() == Some(sid.as_str())); - assert_eq!(&plan.args[2..], &["--resume".to_string(), "abc".to_string()]); + assert_eq!( + &plan.args[2..], + &["--resume".to_string(), "abc".to_string()] + ); // Env override carries the same id so a nested `burn …` inherits it. assert!(plan .env_overrides diff --git a/crates/relayburn-cli/src/harnesses/opencode.rs b/crates/relayburn-cli/src/harnesses/opencode.rs index 8488c5d0..2d2fcfb6 100644 --- a/crates/relayburn-cli/src/harnesses/opencode.rs +++ b/crates/relayburn-cli/src/harnesses/opencode.rs @@ -39,7 +39,11 @@ fn opencode_sessions_dir() -> PathBuf { /// calls this once at lazy-init time. See /// [`pending_stamp::session_store_adapter`] for the leak semantics. pub fn adapter() -> &'static dyn HarnessAdapter { - pending_stamp::session_store_adapter("opencode", opencode_sessions_dir, ingest_opencode_sessions) + pending_stamp::session_store_adapter( + "opencode", + opencode_sessions_dir, + ingest_opencode_sessions, + ) } #[cfg(test)] diff --git a/crates/relayburn-cli/src/harnesses/pending_stamp.rs b/crates/relayburn-cli/src/harnesses/pending_stamp.rs index 8d6a1c4a..412530c5 100644 --- a/crates/relayburn-cli/src/harnesses/pending_stamp.rs +++ b/crates/relayburn-cli/src/harnesses/pending_stamp.rs @@ -136,8 +136,7 @@ pub fn adapter_static(config: PendingStampAdapter) -> &'static dyn HarnessAdapte /// per-tick `Box::pin` adaptation that drops them into [`IngestSessionsFn`] /// happens at the call site in [`session_store_adapter`] so the helper /// stays a fn pointer (no per-tick closure allocation). -pub type SessionIngestor = - fn(&mut RawLedger, &RawIngestOptions) -> anyhow::Result; +pub type SessionIngestor = fn(&mut RawLedger, &RawIngestOptions) -> anyhow::Result; /// One-call factory for pending-stamp adapters whose only differences are /// the harness name, the session-root resolver, and which SDK ingest pass @@ -175,7 +174,11 @@ pub fn session_store_adapter( ingestor(handle.raw_mut(), &opts) }) }); - adapter_static(PendingStampAdapter::new(name, session_root, ingest_sessions)) + adapter_static(PendingStampAdapter::new( + name, + session_root, + ingest_sessions, + )) } /// `HarnessAdapter` implementation backing the [`adapter`] factory. Kept @@ -260,9 +263,8 @@ impl HarnessAdapter for PendingStampAdapterImpl { spawn_start_ts: Some(ctx.spawn_start_ts), spawner_pid: None, }; - let written = write_pending_stamp(opts).map_err(|err| { - anyhow::anyhow!("failed to write {} pending stamp: {err}", self.name) - })?; + let written = write_pending_stamp(opts) + .map_err(|err| anyhow::anyhow!("failed to write {} pending stamp: {err}", self.name))?; eprintln!( "[burn] {} spawn: pending stamp {}", self.name, @@ -271,11 +273,7 @@ impl HarnessAdapter for PendingStampAdapterImpl { Ok(()) } - fn start_watcher( - &self, - ctx: &PlanCtx, - on_report: ReportSink, - ) -> Option { + fn start_watcher(&self, ctx: &PlanCtx, on_report: ReportSink) -> Option { // Match the TS adapter: do not run an immediate first tick. The // child has barely started; let the periodic interval drive the // first scan so we don't spawn an ingest pass that races the diff --git a/crates/relayburn-cli/src/harnesses/registry.rs b/crates/relayburn-cli/src/harnesses/registry.rs index 36e33dc2..8eebc754 100644 --- a/crates/relayburn-cli/src/harnesses/registry.rs +++ b/crates/relayburn-cli/src/harnesses/registry.rs @@ -312,17 +312,18 @@ mod tests { /// actual production wiring path: if `adapter_static`'s return /// type stopped fitting the runtime registry's value bound, this /// test would fail to compile. - static FAKE_PENDING_STAMP_ADAPTER: LazyLock<&'static dyn HarnessAdapter> = LazyLock::new(|| { - let session_root: Arc PathBuf + Send + Sync> = - Arc::new(|| PathBuf::from("/tmp/codex-sessions")); - let ingest_sessions: IngestSessionsFn = - Arc::new(|_ledger_home| Box::pin(async { Ok(IngestReport::default()) })); - pending_stamp::adapter_static(PendingStampAdapter::new( - "codex", - session_root, - ingest_sessions, - )) - }); + static FAKE_PENDING_STAMP_ADAPTER: LazyLock<&'static dyn HarnessAdapter> = + LazyLock::new(|| { + let session_root: Arc PathBuf + Send + Sync> = + Arc::new(|| PathBuf::from("/tmp/codex-sessions")); + let ingest_sessions: IngestSessionsFn = + Arc::new(|_ledger_home| Box::pin(async { Ok(IngestReport::default()) })); + pending_stamp::adapter_static(PendingStampAdapter::new( + "codex", + session_root, + ingest_sessions, + )) + }); /// Module-scoped runtime fake registry. Same value type as the /// production [`RUNTIME_ADAPTERS`] above, so this fixture @@ -363,6 +364,7 @@ mod tests { /// The bound is enforced by storing the function pointer in a /// const with the explicit signature; mismatched types cause a /// compile error here, not a test failure. - const _ASSERT_ADAPTER_STATIC_FITS_REGISTRY: fn(PendingStampAdapter) -> &'static dyn HarnessAdapter = - pending_stamp::adapter_static; + const _ASSERT_ADAPTER_STATIC_FITS_REGISTRY: fn( + PendingStampAdapter, + ) -> &'static dyn HarnessAdapter = pending_stamp::adapter_static; } diff --git a/crates/relayburn-cli/tests/golden.rs b/crates/relayburn-cli/tests/golden.rs index d406dec3..c2129ab7 100644 --- a/crates/relayburn-cli/tests/golden.rs +++ b/crates/relayburn-cli/tests/golden.rs @@ -294,8 +294,7 @@ fn squash_numeric_field(text: &str, key: &str, placeholder: &str) -> String { // below is the right scope. NB: `char::is_ascii_whitespace` is *not* // equivalent — it excludes U+000B (vertical tab), which JS `\s` does // match, so we list the bytes explicitly. - let trimmed_start = after_key - .trim_start_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c')); + let trimmed_start = after_key.trim_start_matches([' ', '\t', '\n', '\r', '\x0b', '\x0c']); let ws_consumed = after_key.len() - trimmed_start.len(); // If the value isn't a bare integer (e.g. `null`), bail and emit // the original bytes untouched. diff --git a/crates/relayburn-sdk-node/src/lib.rs b/crates/relayburn-sdk-node/src/lib.rs index 087dd6f3..c2e776cb 100644 --- a/crates/relayburn-sdk-node/src/lib.rs +++ b/crates/relayburn-sdk-node/src/lib.rs @@ -570,13 +570,7 @@ fn parse_iso_system_time(s: &str) -> std::result::Result let nanos = parse_fractional_nanos(frac_part)?; let max_day = days_in_month(year, month); - if max_day == 0 - || day == 0 - || day > max_day - || hour > 23 - || minute > 59 - || second > 60 - { + if max_day == 0 || day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 { return Err(invalid_arg("spawnStartTs is outside the supported range")); } let days = days_from_civil(year, month, day); diff --git a/crates/relayburn-sdk/src/analyze.rs b/crates/relayburn-sdk/src/analyze.rs index e63082c8..8424d474 100644 --- a/crates/relayburn-sdk/src/analyze.rs +++ b/crates/relayburn-sdk/src/analyze.rs @@ -43,25 +43,25 @@ pub use claude_md::{ SessionClaudeMdCost, TrimRecommendation, }; pub use compare::{ - build_compare_table, compare_from_archive, CompareCategory, CompareCell, CompareFromArchiveResult, - CompareOptions, CompareTable, CompareTotals, DEFAULT_MIN_SAMPLE, + build_compare_table, compare_from_archive, CompareCategory, CompareCell, + CompareFromArchiveResult, CompareOptions, CompareTable, CompareTotals, DEFAULT_MIN_SAMPLE, }; pub use context_delta::{ deltas_for_session, ContextDelta, ContextDeltaOpts, InterveningStep, OwnerFilter, OwnerRail, ReminderSource, }; pub use cost::{cost_for_turn, cost_for_usage, sum_costs, CostBreakdown}; -pub use overhead::{ - attribute_overhead, describe_applies_to, find_overhead_files, load_overhead_file, - AttributeOverheadInput, OverheadAttribution, OverheadFile, OverheadFileAttribution, - OverheadFileKind, ParsedOverheadFile, -}; -pub use patterns::{detect_patterns, DetectPatternsOptions}; pub use fidelity::{ has_minimum_fidelity, summarize_fidelity, summarize_fidelity_from_iter, FidelitySummary, }; pub use findings::{findings_from_patterns, sort_findings, WasteFinding, WasteSeverity}; -pub use ghost_surface::{detect_ghost_surface, ghost_surface_to_finding, GhostSurfaceFindingOptions}; +pub use flow_graph::{ + flow_graph_from_trees, FlowEdge, FlowEdgeKind, FlowGraph, FlowNode, FlowNodeKind, FlowOpts, + TurnTokens, DEFAULT_MAX_TURNS as FLOW_DEFAULT_MAX_TURNS, INTER_TURN_GAP, RAIL_GAP, +}; +pub use ghost_surface::{ + detect_ghost_surface, ghost_surface_to_finding, GhostSurfaceFindingOptions, +}; pub use ghost_surface_inputs::build_ghost_surface_inputs; pub use hotspots::{ aggregate_by_bash, aggregate_by_bash_verb, aggregate_by_file, aggregate_by_mcp_server, @@ -69,6 +69,12 @@ pub use hotspots::{ BashVerbAggregation, FileAggregation, HotspotsOptions, HotspotsResult, McpServerAggregation, SessionTotals, SubagentAggregation, ToolAttribution, }; +pub use overhead::{ + attribute_overhead, describe_applies_to, find_overhead_files, load_overhead_file, + AttributeOverheadInput, OverheadAttribution, OverheadFile, OverheadFileAttribution, + OverheadFileKind, ParsedOverheadFile, +}; +pub use patterns::{detect_patterns, DetectPatternsOptions}; pub use pricing::{load_pricing, ModelCost, PricingTable, ReasoningMode}; pub use provider::{ aggregate_by_provider, filter_turns_by_provider, filter_turns_by_provider_with_rules, @@ -84,10 +90,6 @@ pub use replacement_savings::{ summarize_replacement_savings, ReplacementSavingsSummary, ToolSavingsAggregate, }; pub use span_tree::{AttrValue, SpanEvent, SpanKind, SpanNode, SpanStatus, TurnSpanTree}; -pub use flow_graph::{ - flow_graph_from_trees, FlowEdge, FlowEdgeKind, FlowGraph, FlowNode, FlowNodeKind, FlowOpts, - TurnTokens, DEFAULT_MAX_TURNS as FLOW_DEFAULT_MAX_TURNS, INTER_TURN_GAP, RAIL_GAP, -}; pub use subagent_tree::{ aggregate_subagent_type_stats, build_subagent_tree, BuildSubagentTreeOptions, SubagentTreeNode, SubagentTypeStats, diff --git a/crates/relayburn-sdk/src/analyze/claude_md.rs b/crates/relayburn-sdk/src/analyze/claude_md.rs index 613ea97b..dde7589d 100644 --- a/crates/relayburn-sdk/src/analyze/claude_md.rs +++ b/crates/relayburn-sdk/src/analyze/claude_md.rs @@ -14,9 +14,9 @@ use std::io; use std::path::{Path, PathBuf}; use std::sync::LazyLock; +use crate::reader::TurnRecord; use indexmap::IndexMap; use regex::Regex; -use crate::reader::TurnRecord; use serde::{Deserialize, Serialize}; use crate::analyze::cost::lookup_model_rate; @@ -188,9 +188,14 @@ pub fn parse_claude_md(file_path: &str, text: &str) -> ParsedClaudeMd { }; } - let group_headings: Vec<&HeadingInfo> = - headings.iter().filter(|h| h.level == grouping_level).collect(); - let first_start = group_headings.first().map(|h| h.line).unwrap_or(total_lines + 1); + let group_headings: Vec<&HeadingInfo> = headings + .iter() + .filter(|h| h.level == grouping_level) + .collect(); + let first_start = group_headings + .first() + .map(|h| h.line) + .unwrap_or(total_lines + 1); if first_start > 1 { let pb_bytes = range_bytes(1, first_start - 1); if pb_bytes > 0 { @@ -929,11 +934,19 @@ mod tests { assert_eq!(files.len(), 2); assert!(files.iter().any(|f| { f.file_name().unwrap() == "CLAUDE.md" - && f.parent().unwrap().file_name().map(|s| s != ".claude").unwrap_or(true) + && f.parent() + .unwrap() + .file_name() + .map(|s| s != ".claude") + .unwrap_or(true) })); assert!(files.iter().any(|f| { f.file_name().unwrap() == "CLAUDE.md" - && f.parent().unwrap().file_name().map(|s| s == ".claude").unwrap_or(false) + && f.parent() + .unwrap() + .file_name() + .map(|s| s == ".claude") + .unwrap_or(false) })); } diff --git a/crates/relayburn-sdk/src/analyze/compare.rs b/crates/relayburn-sdk/src/analyze/compare.rs index fdd06b8b..75b41d8a 100644 --- a/crates/relayburn-sdk/src/analyze/compare.rs +++ b/crates/relayburn-sdk/src/analyze/compare.rs @@ -144,16 +144,16 @@ pub fn build_compare_table(turns: &[EnrichedTurn], opts: &CompareOptions<'_>) -> if !by_model_category.contains_key(model) { let owned = model.to_string(); model_set.insert(owned.clone(), ()); - model_totals - .entry(owned.clone()) - .or_insert_with(CompareTotals::default); + model_totals.entry(owned.clone()).or_default(); by_model_category.insert(owned, BTreeMap::new()); } if !category_set.contains_key(&cat) { category_set.insert(cat.clone(), ()); } - let by_cat = by_model_category.get_mut(model).expect("model just inserted"); + let by_cat = by_model_category + .get_mut(model) + .expect("model just inserted"); let acc = by_cat.entry(cat).or_default(); acc.turns += 1; diff --git a/crates/relayburn-sdk/src/analyze/context_delta.rs b/crates/relayburn-sdk/src/analyze/context_delta.rs index 297c8fbf..5e197ee6 100644 --- a/crates/relayburn-sdk/src/analyze/context_delta.rs +++ b/crates/relayburn-sdk/src/analyze/context_delta.rs @@ -339,7 +339,7 @@ pub fn deltas_for_session( ms >= prev_end && ms <= curr_start }); let (delta_tokens, intervening) = if raw_delta < 0 && compaction_between { - let freed = (prev_ctx - curr_ctx) as u64; + let freed = prev_ctx - curr_ctx; let mut steps = intervening; steps.push(InterveningStep::Compaction { tokens_freed: freed, @@ -387,7 +387,9 @@ pub fn deltas_for_session( .cmp(&a.delta_tokens) .then_with(|| a.turn_id.cmp(&b.turn_id)) .then_with(|| a.inference_idx.cmp(&b.inference_idx)) - .then_with(|| owner_rail_sort_key(&a.owner_rail).cmp(&owner_rail_sort_key(&b.owner_rail))) + .then_with(|| { + owner_rail_sort_key(&a.owner_rail).cmp(&owner_rail_sort_key(&b.owner_rail)) + }) .then_with(|| a.session_id.cmp(&b.session_id)) }); @@ -409,12 +411,12 @@ fn owner_rail_sort_key(rail: &OwnerRail) -> (&str, &str) { } fn rail_passes_filter(rail: &OwnerRail, filter: OwnerFilter) -> bool { - match (rail, filter) { - (_, OwnerFilter::All) => true, - (OwnerRail::Main, OwnerFilter::Main) => true, - (OwnerRail::Subagent { .. }, OwnerFilter::Subagent) => true, - _ => false, - } + matches!( + (rail, filter), + (_, OwnerFilter::All) + | (OwnerRail::Main, OwnerFilter::Main) + | (OwnerRail::Subagent { .. }, OwnerFilter::Subagent) + ) } fn attributed_cost(delta_tokens: i64, model: &str, pricing: &PricingTable) -> f64 { @@ -657,7 +659,13 @@ mod tests { use crate::analyze::span_tree::{SpanKind, SpanNode, SpanStatus, TurnSpanTree}; use crate::reader::{CompactionEvent, SourceKind}; - fn make_inf(req_id: &str, model: &str, input: i64, cache_read: i64, cache_write: i64) -> SpanNode { + fn make_inf( + req_id: &str, + model: &str, + input: i64, + cache_read: i64, + cache_write: i64, + ) -> SpanNode { let mut n = SpanNode::new(SpanKind::Inference, model); n.set_attr("model", AttrValue::str(model)); n.set_attr("request_id", AttrValue::str(req_id)); @@ -707,7 +715,9 @@ mod tests { // inference #1: context = 1000 let inf1 = make_inf("req-1", "claude-sonnet-4-6", 1000, 0, 0); let mut bash_use = make_tool_use("Bash", "tu-1"); - bash_use.children.push(make_tool_result("tu-1", 40_000, false)); + bash_use + .children + .push(make_tool_result("tu-1", 40_000, false)); let mut inf1 = inf1; inf1.children.push(bash_use); @@ -924,11 +934,7 @@ mod tests { ..ContextDeltaOpts::default() }; let deltas = deltas_for_session( - &[turn_tree( - "sess-1", - "msg-1", - root_with_two_infs(1000, 1500), - )], + &[turn_tree("sess-1", "msg-1", root_with_two_infs(1000, 1500))], &[], &pricing, &opts, @@ -955,13 +961,8 @@ mod tests { root.children.push(make_user_prompt()); let ctx_steps = [1000, 6000, 11_000, 16_000, 21_000]; for (i, c) in ctx_steps.iter().enumerate() { - root.children.push(make_inf( - &format!("req-{i}"), - "claude-sonnet-4-6", - *c, - 0, - 0, - )); + root.children + .push(make_inf(&format!("req-{i}"), "claude-sonnet-4-6", *c, 0, 0)); } let tree = turn_tree("sess-1", "msg-1", root); let pricing = crate::analyze::pricing::load_builtin_pricing(); @@ -971,7 +972,7 @@ mod tests { min_delta: Some(0), ..ContextDeltaOpts::default() }; - let all = deltas_for_session(&[tree.clone()], &[], &pricing, &opts); + let all = deltas_for_session(std::slice::from_ref(&tree), &[], &pricing, &opts); assert_eq!(all.len(), 4); // Cap at 2 → only the top 2 deltas. diff --git a/crates/relayburn-sdk/src/analyze/fidelity.rs b/crates/relayburn-sdk/src/analyze/fidelity.rs index a0cc3e88..ce1344d1 100644 --- a/crates/relayburn-sdk/src/analyze/fidelity.rs +++ b/crates/relayburn-sdk/src/analyze/fidelity.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use serde::Serialize; use crate::reader::{ - Coverage, Fidelity, FidelityClass, TurnRecord, UsageGranularity, classify_fidelity, + classify_fidelity, Coverage, Fidelity, FidelityClass, TurnRecord, UsageGranularity, }; /// Names of the boolean fields on [`Coverage`]. Mirrors the TS `keyof Coverage` diff --git a/crates/relayburn-sdk/src/analyze/flow_graph.rs b/crates/relayburn-sdk/src/analyze/flow_graph.rs index 339099b9..ece0d19c 100644 --- a/crates/relayburn-sdk/src/analyze/flow_graph.rs +++ b/crates/relayburn-sdk/src/analyze/flow_graph.rs @@ -428,14 +428,8 @@ impl Builder { // "return" to a main-rail successor — they connect // via an `Unattached` edge from the most recent main // node and that's the end of the chain). - let first_id = self.emit_subagent_rail( - tree, - child, - rail, - turn_x, - unattached_y, - None, - ); + let first_id = + self.emit_subagent_rail(tree, child, rail, turn_x, unattached_y, None); if let Some(first_id) = first_id { let anchor = prev_main_id.clone().or_else(|| { self.last_main_node_per_turn @@ -479,8 +473,7 @@ impl Builder { y_anchor: i32, dispatch_inference_index: Option, ) -> Option { - let agent_id = span_string(sub, "agent_id") - .unwrap_or_else(|| format!("rail-{rail}")); + let agent_id = span_string(sub, "agent_id").unwrap_or_else(|| format!("rail-{rail}")); let label = if !sub.name.is_empty() { sub.name.clone() } else { @@ -647,9 +640,8 @@ impl Builder { // from_idx)`. `partition_point` gives us the // insertion index immediately past the matching // dispatch entry, which is exactly the successor. - let pivot = timeline.partition_point(|(t, i, _)| { - (*t, *i) <= (from_turn, from_idx) - }); + let pivot = + timeline.partition_point(|(t, i, _)| (*t, *i) <= (from_turn, from_idx)); if let Some((_, _, id)) = timeline.get(pivot) { resolved.push(FlowEdge { from: edge.from, @@ -710,11 +702,7 @@ fn span_duration(node: &SpanNode) -> i64 { /// buffered cross-rail / unattached edges, and assembles the final /// `FlowGraph` value. Kept private — callers go through one of the /// public entrypoints so the projection contract has one home. -fn build_with_finalize( - session_id: &str, - trees: &[TurnSpanTree], - opts: FlowOpts, -) -> FlowGraph { +fn build_with_finalize(session_id: &str, trees: &[TurnSpanTree], opts: FlowOpts) -> FlowGraph { let total_turn_count = u32::try_from(trees.len()).unwrap_or(u32::MAX); let max_turns = opts.effective_max_turns(); let take = match max_turns { @@ -932,11 +920,7 @@ mod tests { root.children.push(inf0); root.children.push(inf1); - let graph = flow_graph_from_trees( - "sess-1", - &[make_tree(0, root)], - FlowOpts::default(), - ); + let graph = flow_graph_from_trees("sess-1", &[make_tree(0, root)], FlowOpts::default()); let return_edge = graph .edges @@ -992,17 +976,16 @@ mod tests { inf.children.push(task); let mut root = turn_root(); root.children.push(inf); - let graph = flow_graph_from_trees( - "sess-1", - &[make_tree(0, root)], - FlowOpts::default(), - ); + let graph = flow_graph_from_trees("sess-1", &[make_tree(0, root)], FlowOpts::default()); assert!( !graph.edges.iter().any(|e| e.kind == FlowEdgeKind::Return), "no Return edge expected when dispatch is the terminal inference" ); assert!( - !graph.edges.iter().any(|e| e.to.starts_with("__return_anchor")), + !graph + .edges + .iter() + .any(|e| e.to.starts_with("__return_anchor")), "no unresolved placeholders should leak into the final graph" ); } @@ -1054,11 +1037,7 @@ mod tests { make_tree(i, root) }) .collect(); - let graph = flow_graph_from_trees( - "sess-1", - &trees, - FlowOpts { max_turns: Some(3) }, - ); + let graph = flow_graph_from_trees("sess-1", &trees, FlowOpts { max_turns: Some(3) }); assert_eq!(graph.turn_count, 3); assert_eq!(graph.total_turn_count, 10); assert!(graph.truncated); @@ -1074,11 +1053,7 @@ mod tests { make_tree(i, root) }) .collect(); - let graph = flow_graph_from_trees( - "sess-1", - &trees, - FlowOpts { max_turns: Some(0) }, - ); + let graph = flow_graph_from_trees("sess-1", &trees, FlowOpts { max_turns: Some(0) }); assert_eq!(graph.turn_count, 3); assert!(!graph.truncated); } diff --git a/crates/relayburn-sdk/src/analyze/ghost_surface.rs b/crates/relayburn-sdk/src/analyze/ghost_surface.rs index ee7773cf..6024f5df 100644 --- a/crates/relayburn-sdk/src/analyze/ghost_surface.rs +++ b/crates/relayburn-sdk/src/analyze/ghost_surface.rs @@ -38,8 +38,8 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::LazyLock; -use regex::Regex; use crate::reader::SourceKind; +use regex::Regex; use serde::{Deserialize, Serialize}; use crate::analyze::findings::{EstimatedSavings, WasteAction, WasteFinding, WasteSeverity}; @@ -52,6 +52,7 @@ use crate::analyze::findings::{EstimatedSavings, WasteAction, WasteFinding, Wast /// as the TS string-literal union. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] +#[allow(clippy::enum_variant_names)] pub enum GhostFindingKind { GhostAgent, GhostSkill, diff --git a/crates/relayburn-sdk/src/analyze/hotspots.rs b/crates/relayburn-sdk/src/analyze/hotspots.rs index 3a1d5ea2..36f976c5 100644 --- a/crates/relayburn-sdk/src/analyze/hotspots.rs +++ b/crates/relayburn-sdk/src/analyze/hotspots.rs @@ -9,12 +9,12 @@ use std::collections::HashMap; -use indexmap::IndexMap; -use phf::phf_set; use crate::reader::{ BashParse, ContentKind, ContentRecord, ToolResultEventRecord, TurnRecord, UserTurnBlockKind, UserTurnRecord, }; +use indexmap::IndexMap; +use phf::phf_set; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -479,7 +479,8 @@ fn attribute_session( if new_content > 0.0 { let input_share = turn.usage.input as f64 / new_content; let create_share = 1.0 - input_share; - let per_token_price = input_share * rate.input + create_share * rate.cache_write; + let per_token_price = + input_share * rate.input + create_share * rate.cache_write; if have_any_sizes { let sibling_total: f64 = pending_initial .iter() @@ -571,7 +572,10 @@ fn attribute_session( } else { None }; - let bytes_entry = bytes_by_tool_use_id.get(&tc.id).cloned().unwrap_or_default(); + let bytes_entry = bytes_by_tool_use_id + .get(&tc.id) + .cloned() + .unwrap_or_default(); attributions.push(ToolAttribution { tool_use_id: tc.id.clone(), tool_name: tc.name.clone(), @@ -903,11 +907,8 @@ where .total_cmp(&a.total_cost) .then_with(|| a.command.cmp(&b.command)) }); - let top_examples: Vec = examples - .into_iter() - .take(3) - .map(|e| e.command) - .collect(); + let top_examples: Vec = + examples.into_iter().take(3).map(|e| e.command).collect(); BashVerbAggregation { verb: row.verb, call_count: row.call_count, @@ -1083,6 +1084,7 @@ mod tests { } } + #[allow(clippy::too_many_arguments)] fn turn( session_id: &str, message_id: &str, @@ -1171,11 +1173,7 @@ mod tests { } } - fn user_turn( - session_id: &str, - user_uuid: &str, - blocks: Vec, - ) -> UserTurnRecord { + fn user_turn(session_id: &str, user_uuid: &str, blocks: Vec) -> UserTurnRecord { UserTurnRecord { v: 1, source: SourceKind::ClaudeCode, @@ -1234,7 +1232,10 @@ mod tests { #[test] fn attributes_persistence_of_8k_read_across_20_ride_along_turns_within_10_pct() { let pricing = load_builtin_pricing(); - let rate = pricing.get("claude-sonnet-4-6").expect("sonnet present").clone(); + let rate = pricing + .get("claude-sonnet-4-6") + .expect("sonnet present") + .clone(); const READ_TOKENS: u64 = 8000; let read_text: String = "x".repeat((READ_TOKENS as usize) * 4); @@ -1569,7 +1570,12 @@ mod tests { "2026-04-20T00:00:00.000Z", "claude-sonnet-4-6", empty_usage(), - vec![tc_with_hash("tu_a1", "Agent", "general-purpose", "Agent:gp")], + vec![tc_with_hash( + "tu_a1", + "Agent", + "general-purpose", + "Agent:gp", + )], SourceKind::ClaudeCode, ), turn( @@ -1981,15 +1987,8 @@ mod tests { tool_result_events_by_session: None, }, ); - let summed: f64 = result - .attributions - .iter() - .map(|a| a.initial_tokens) - .sum(); - assert!( - summed <= 5000.0 + 1e-6, - "summed={summed} > newContent=5000" - ); + let summed: f64 = result.attributions.iter().map(|a| a.initial_tokens).sum(); + assert!(summed <= 5000.0 + 1e-6, "summed={summed} > newContent=5000"); let big = result .attributions .iter() diff --git a/crates/relayburn-sdk/src/analyze/overhead.rs b/crates/relayburn-sdk/src/analyze/overhead.rs index 245168f7..cbcb0e17 100644 --- a/crates/relayburn-sdk/src/analyze/overhead.rs +++ b/crates/relayburn-sdk/src/analyze/overhead.rs @@ -132,11 +132,8 @@ pub fn attribute_overhead(input: AttributeOverheadInput<'_>) -> OverheadAttribut .iter() .filter(|t| pf.file.applies_to.contains(&t.source)) .collect(); - let attribution = attribute_claude_md_refs( - std::slice::from_ref(&pf.parsed), - &filtered, - input.pricing, - ); + let attribution = + attribute_claude_md_refs(std::slice::from_ref(&pf.parsed), &filtered, input.pricing); for sc in &attribution.session_costs { let prev = max_riding_by_session @@ -236,8 +233,14 @@ mod tests { let files = find_overhead_files(root); assert_eq!(files.len(), 3); - let agents = files.iter().find(|f| f.kind == OverheadFileKind::AgentsMd).unwrap(); - assert_eq!(agents.applies_to, vec![SourceKind::Codex, SourceKind::Opencode]); + let agents = files + .iter() + .find(|f| f.kind == OverheadFileKind::AgentsMd) + .unwrap(); + assert_eq!( + agents.applies_to, + vec![SourceKind::Codex, SourceKind::Opencode] + ); let claude_count = files .iter() .filter(|f| f.kind == OverheadFileKind::ClaudeMd) @@ -253,14 +256,10 @@ mod tests { #[test] fn routes_turns_by_source_and_grand_total_matches_per_file_sum_within_1e_9() { let pricing = pricing_with("claude-sonnet-4-6", 0.30); - let claude_md = parse_claude_md( - "/p/CLAUDE.md", - &format!("## Claude\n{}", "c".repeat(4000)), - ); - let agents_md = parse_claude_md( - "/p/AGENTS.md", - &format!("## Agents\n{}", "a".repeat(4000)), - ); + let claude_md = + parse_claude_md("/p/CLAUDE.md", &format!("## Claude\n{}", "c".repeat(4000))); + let agents_md = + parse_claude_md("/p/AGENTS.md", &format!("## Agents\n{}", "a".repeat(4000))); let files = vec![ ParsedOverheadFile { @@ -307,10 +306,7 @@ mod tests { // Claude Code session attributes only to CLAUDE.md. assert_eq!(claude_attr.attribution.session_count, 1); - assert_eq!( - claude_attr.attribution.session_costs[0].session_id, - "s-cc" - ); + assert_eq!(claude_attr.attribution.session_costs[0].session_id, "s-cc"); let expected_claude = (claude_md.tokens as f64 / 1_000_000.0) * 0.30; assert!( (claude_attr.attribution.total_cost - expected_claude).abs() <= expected_claude * 0.10, @@ -323,8 +319,7 @@ mod tests { assert_eq!(agents_attr.attribution.session_count, 2); let expected_agents = 2.0 * (agents_md.tokens as f64 / 1_000_000.0) * 0.30; assert!( - (agents_attr.attribution.total_cost - expected_agents).abs() - <= expected_agents * 0.10, + (agents_attr.attribution.total_cost - expected_agents).abs() <= expected_agents * 0.10, "agents cost={} expected~{}", agents_attr.attribution.total_cost, expected_agents @@ -364,10 +359,20 @@ mod tests { ]; let mut turns: Vec = Vec::new(); for i in 0..5 { - turns.push(mk_turn("s-both", i, SourceKind::ClaudeCode, big.tokens + 1000)); + turns.push(mk_turn( + "s-both", + i, + SourceKind::ClaudeCode, + big.tokens + 1000, + )); } for i in 5..8 { - turns.push(mk_turn("s-both", i, SourceKind::ClaudeCode, small.tokens + 500)); + turns.push(mk_turn( + "s-both", + i, + SourceKind::ClaudeCode, + small.tokens + 500, + )); } let result = attribute_overhead(AttributeOverheadInput { files: &files, @@ -512,7 +517,12 @@ mod tests { let mut summed_per_file = 0.0_f64; for fa in &result.per_file { summed_per_file += fa.attribution.total_cost; - let sec_sum: f64 = fa.attribution.section_costs.iter().map(|s| s.total_cost).sum(); + let sec_sum: f64 = fa + .attribution + .section_costs + .iter() + .map(|s| s.total_cost) + .sum(); assert!(sec_sum <= fa.attribution.total_cost + 1e-9); } assert!((result.grand_total - summed_per_file).abs() < 1e-9); diff --git a/crates/relayburn-sdk/src/analyze/patterns.rs b/crates/relayburn-sdk/src/analyze/patterns.rs index b5cd2ced..b8a4c47e 100644 --- a/crates/relayburn-sdk/src/analyze/patterns.rs +++ b/crates/relayburn-sdk/src/analyze/patterns.rs @@ -15,7 +15,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; - use crate::reader::{ count_retries, normalize_tool_name, CompactionEvent, ContentKind, ContentRecord, ContentToolResult, ContentToolUse, SourceKind, ToolCall, ToolResultEventRecord, @@ -27,8 +26,8 @@ use crate::analyze::cost::{cost_for_turn, cost_for_usage, CostForUsageOptions}; use crate::analyze::findings::{ CancellationRun, CompactionLoss, CompactionLostWork, EditHeavySession, EditPreview, EditRevertCycle, EditRevertSamplePreview, FailureRun, FailureRunErrorSignature, - PatternEventSource, PatternsResult, RetryLoop, SessionPatternSummary, - SkillPruningProtection, SkillRecallDup, SystemPromptTax, + PatternEventSource, PatternsResult, RetryLoop, SessionPatternSummary, SkillPruningProtection, + SkillRecallDup, SystemPromptTax, }; use crate::analyze::pricing::PricingTable; @@ -219,7 +218,9 @@ pub fn detect_patterns(turns: &[TurnRecord], opts: &DetectPatternsOptions<'_>) - } let compactions = match opts.compactions { - Some(events) => detect_compaction_losses(events, turns, opts.pricing, opts.content_by_session), + Some(events) => { + detect_compaction_losses(events, turns, opts.pricing, opts.content_by_session) + } None => Vec::new(), }; @@ -1064,10 +1065,7 @@ pub(crate) fn detect_edit_reverts_for_session<'a>( first_edit_turn_index: first.turn.turn_index, revert_turn_index: r.turn.turn_index, span_turns: r.turn.turn_index - first.turn.turn_index, - cost: sum_cost_for_turns( - &dedup_turns(vec![first.turn, r.turn]), - pricing, - ), + cost: sum_cost_for_turns(&dedup_turns(vec![first.turn, r.turn]), pricing), sample_preview: None, }; if let Some(content_idx) = content_index { @@ -1084,10 +1082,7 @@ pub(crate) fn detect_edit_reverts_for_session<'a>( .map(|tu| &tu.input), ); if let (Some(first_edit), Some(revert)) = (first_edit, revert) { - cycle.sample_preview = Some(EditRevertSamplePreview { - first_edit, - revert, - }); + cycle.sample_preview = Some(EditRevertSamplePreview { first_edit, revert }); } } cycles.push(cycle); @@ -1124,7 +1119,10 @@ fn detect_compaction_losses( if !events_by_session.contains_key(&e.session_id) { events_order.push(e.session_id.clone()); } - events_by_session.entry(e.session_id.clone()).or_default().push(e); + events_by_session + .entry(e.session_id.clone()) + .or_default() + .push(e); } for list in events_by_session.values_mut() { list.sort_by(|a, b| a.ts.cmp(&b.ts)); @@ -1133,7 +1131,10 @@ fn detect_compaction_losses( // Sort turns by session, then turn_index. let mut turns_by_session: HashMap> = HashMap::new(); for t in turns { - turns_by_session.entry(t.session_id.clone()).or_default().push(t); + turns_by_session + .entry(t.session_id.clone()) + .or_default() + .push(t); } for list in turns_by_session.values_mut() { list.sort_by_key(|t| t.turn_index); @@ -1325,7 +1326,9 @@ pub(crate) fn detect_skill_pruning_protection_for_session( if riding_turns == 0 { continue; } - let invoke_cost = cost_for_turn(r.turn, pricing).map(|c| c.total).unwrap_or(0.0); + let invoke_cost = cost_for_turn(r.turn, pricing) + .map(|c| c.total) + .unwrap_or(0.0); out.push(SkillPruningProtection { session_id: session_id.to_string(), skill_name, @@ -1348,8 +1351,7 @@ pub(crate) fn detect_system_prompt_tax_for_session( return Vec::new(); } let first_turn = turns[0]; - let first_cache_create = - first_turn.usage.cache_create_5m + first_turn.usage.cache_create_1h; + let first_cache_create = first_turn.usage.cache_create_5m + first_turn.usage.cache_create_1h; if first_cache_create == 0 { return Vec::new(); } diff --git a/crates/relayburn-sdk/src/analyze/patterns_tests.rs b/crates/relayburn-sdk/src/analyze/patterns_tests.rs index 52daa675..e8191a6a 100644 --- a/crates/relayburn-sdk/src/analyze/patterns_tests.rs +++ b/crates/relayburn-sdk/src/analyze/patterns_tests.rs @@ -66,14 +66,7 @@ fn tc_target(id: &str, name: &str, args_hash: &str, target: &str) -> ToolCall { c } -fn tc_edit( - id: &str, - name: &str, - args_hash: &str, - target: &str, - pre: &str, - post: &str, -) -> ToolCall { +fn tc_edit(id: &str, name: &str, args_hash: &str, target: &str, pre: &str, post: &str) -> ToolCall { let mut c = tc_target(id, name, args_hash, target); c.edit_pre_hash = Some(pre.into()); c.edit_post_hash = Some(post.into()); @@ -156,12 +149,13 @@ fn evt( } } -fn evt_subagent( - session_id: &str, - tool_use_id: &str, - event_index: u64, -) -> ToolResultEventRecord { - let mut e = evt(session_id, tool_use_id, event_index, ToolResultStatus::Errored); +fn evt_subagent(session_id: &str, tool_use_id: &str, event_index: u64) -> ToolResultEventRecord { + let mut e = evt( + session_id, + tool_use_id, + event_index, + ToolResultStatus::Errored, + ); e.event_source = ToolResultEventSource::SubagentNotification; e } @@ -265,7 +259,7 @@ mod retry_loops { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 1); @@ -289,7 +283,7 @@ mod retry_loops { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); let graph = detect_patterns( @@ -299,7 +293,7 @@ mod retry_loops { tool_result_events: Some(&res.tool_result_events), compactions: None, user_turns_by_session: None, - content_by_session: None + content_by_session: None, }, ); assert_eq!(graph.retry_loops.len(), 1); @@ -332,7 +326,7 @@ mod retry_loops { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 0); @@ -358,7 +352,7 @@ mod retry_loops { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 0); @@ -391,7 +385,7 @@ mod retry_loops { tool_result_events: Some(&events), compactions: None, user_turns_by_session: None, - content_by_session: None + content_by_session: None, }, ); assert_eq!(result.retry_loops.len(), 2); @@ -430,7 +424,7 @@ mod consecutive_failure_runs { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.failure_runs.len(), 1); @@ -459,7 +453,7 @@ mod consecutive_failure_runs { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.failure_runs.len(), 0); @@ -478,22 +472,29 @@ mod consecutive_failure_runs { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 1, "retry loop reported"); - assert_eq!(result.failure_runs.len(), 0, "same-key streak is NOT a failure run"); + assert_eq!( + result.failure_runs.len(), + 0, + "same-key streak is NOT a failure run" + ); } #[test] fn counts_chained_subagent_notification_errors() { let pricing = load_builtin_pricing(); let mut t1 = turn("s", "m1", 0); - t1.tool_calls.push(tc_target("a1", "Agent", "agent:one", "agent-a")); + t1.tool_calls + .push(tc_target("a1", "Agent", "agent:one", "agent-a")); let mut t2 = turn("s", "m2", 1); - t2.tool_calls.push(tc_target("a2", "Agent", "agent:two", "agent-b")); + t2.tool_calls + .push(tc_target("a2", "Agent", "agent:two", "agent-b")); let mut t3 = turn("s", "m3", 2); - t3.tool_calls.push(tc_target("a3", "Agent", "agent:three", "agent-c")); + t3.tool_calls + .push(tc_target("a3", "Agent", "agent:three", "agent-c")); let events = vec![ evt_subagent("s", "a1", 0), evt_subagent("s", "a2", 1), @@ -506,7 +507,7 @@ mod consecutive_failure_runs { tool_result_events: Some(&events), compactions: None, user_turns_by_session: None, - content_by_session: None + content_by_session: None, }, ); assert_eq!(result.failure_runs.len(), 1); @@ -546,7 +547,7 @@ mod cancelled_graph_events { tool_result_events: Some(&events), compactions: None, user_turns_by_session: None, - content_by_session: None + content_by_session: None, }, ); assert_eq!(result.retry_loops.len(), 0); @@ -582,7 +583,7 @@ mod compaction_losses { compactions: Some(&res.events), user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.compactions.len(), 1); @@ -603,8 +604,9 @@ mod edit_reverts { #[test] fn detects_two_edit_cycle_where_b_reverts_a() { let pricing = load_builtin_pricing(); - let res = parse_claude_session(fixture("edit-revert.jsonl"), &ClaudeParseOptions::default()) - .expect("parse edit-revert fixture"); + let res = + parse_claude_session(fixture("edit-revert.jsonl"), &ClaudeParseOptions::default()) + .expect("parse edit-revert fixture"); let result = detect_patterns( &res.turns, &DetectPatternsOptions { @@ -612,7 +614,7 @@ mod edit_reverts { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_reverts.len(), 1); @@ -639,7 +641,7 @@ mod edit_reverts { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_reverts.len(), 0); @@ -664,7 +666,7 @@ mod edit_reverts { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_reverts.len(), 1); @@ -705,7 +707,7 @@ mod session_summary_rollup { compactions: Some(&compact.events), user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); @@ -751,7 +753,7 @@ mod defensive { compactions: Some(&no_compactions), user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert!(result.retry_loops.is_empty()); @@ -770,7 +772,12 @@ mod defensive { mod opencode_skill_recall_dups { use super::*; - fn opencode_turn_with_skill(message_id: &str, turn_index: u64, id: &str, skill: &str) -> TurnRecord { + fn opencode_turn_with_skill( + message_id: &str, + turn_index: u64, + id: &str, + skill: &str, + ) -> TurnRecord { turn_with( "s", message_id, @@ -796,7 +803,7 @@ mod opencode_skill_recall_dups { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_recall_dups.len(), 1); @@ -818,7 +825,7 @@ mod opencode_skill_recall_dups { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_recall_dups.len(), 0); @@ -838,7 +845,7 @@ mod opencode_skill_recall_dups { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_recall_dups.len(), 0); @@ -860,7 +867,7 @@ mod opencode_skill_recall_dups { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_recall_dups.len(), 2); @@ -933,7 +940,7 @@ mod opencode_skill_pruning_protection { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_pruning_protection.len(), 1); @@ -972,7 +979,7 @@ mod opencode_skill_pruning_protection { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_pruning_protection.len(), 0); @@ -1006,7 +1013,7 @@ mod opencode_skill_pruning_protection { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.skill_pruning_protection.len(), 0); @@ -1072,7 +1079,7 @@ mod session_summary_includes_skill_detectors { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); let summary = result @@ -1129,7 +1136,7 @@ mod opencode_system_prompt_tax { user_turns_by_session: Some(&user_turns_by_session), compactions: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.system_prompt_taxes.len(), 1); @@ -1159,7 +1166,7 @@ mod opencode_system_prompt_tax { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.system_prompt_taxes.len(), 0); @@ -1176,7 +1183,14 @@ mod opencode_system_prompt_tax { cache_create_5m: 5200, cache_create_1h: 0, }; - let turns = vec![turn_with("s", "m1", 0, SourceKind::ClaudeCode, usage, vec![])]; + let turns = vec![turn_with( + "s", + "m1", + 0, + SourceKind::ClaudeCode, + usage, + vec![], + )]; let mut user_turns_by_session: HashMap> = HashMap::new(); user_turns_by_session.insert( "s".into(), @@ -1189,7 +1203,7 @@ mod opencode_system_prompt_tax { user_turns_by_session: Some(&user_turns_by_session), compactions: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.system_prompt_taxes.len(), 0); @@ -1230,7 +1244,7 @@ mod opencode_system_prompt_tax { user_turns_by_session: Some(&user_turns_by_session), compactions: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.system_prompt_taxes.len(), 1); @@ -1261,7 +1275,7 @@ mod opencode_system_prompt_tax { user_turns_by_session: Some(&user_turns_by_session), compactions: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.system_prompt_taxes.len(), 0); @@ -1302,7 +1316,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1324,7 +1338,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1343,7 +1357,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1372,7 +1386,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 0); @@ -1400,7 +1414,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 0); @@ -1435,7 +1449,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1467,7 +1481,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 0); @@ -1493,7 +1507,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1507,7 +1521,8 @@ mod edit_heavy_sessions { let mut turns = edit_heavy_turns(SourceKind::Codex, "apply_patch", "s"); let mut b1 = turn("s", "b1", 6); b1.source = SourceKind::Codex; - b1.tool_calls.push(tc_target("b1", "shell", "a", "git status")); + b1.tool_calls + .push(tc_target("b1", "shell", "a", "git status")); let mut b2 = turn("s", "b2", 7); b2.source = SourceKind::Codex; b2.tool_calls.push(tc_target( @@ -1525,7 +1540,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1551,7 +1566,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); // 6 edits + 2 reads → ratio 3.0, ≤ 4: no flag. @@ -1592,7 +1607,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_heavy_sessions.len(), 1); @@ -1612,7 +1627,7 @@ mod edit_heavy_sessions { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); let summary = result @@ -1649,7 +1664,12 @@ mod retry_loop_error_signature { content_by_session.insert( "s".into(), vec![ - tool_result_record("s", "m0", "u0", "npm ERR! code ENOENT\n more details\n more details"), + tool_result_record( + "s", + "m0", + "u0", + "npm ERR! code ENOENT\n more details\n more details", + ), tool_result_record("s", "m1", "u1", "npm ERR! code ENOENT\n more details"), tool_result_record("s", "m2", "u2", "npm ERR! code ENOENT\n trailing"), tool_result_record("s", "m3", "u3", "npm ERR! code ENOENT\n yet again"), @@ -1662,7 +1682,7 @@ mod retry_loop_error_signature { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 1); @@ -1692,7 +1712,7 @@ mod retry_loop_error_signature { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 1); @@ -1713,7 +1733,7 @@ mod retry_loop_error_signature { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 1); @@ -1727,7 +1747,12 @@ mod retry_loop_error_signature { let mut content_by_session: HashMap> = HashMap::new(); content_by_session.insert( "s".into(), - vec![tool_result_record("s", "m99", "unrelated", "something else")], + vec![tool_result_record( + "s", + "m99", + "unrelated", + "something else", + )], ); let result = detect_patterns( &turns, @@ -1736,7 +1761,7 @@ mod retry_loop_error_signature { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.retry_loops.len(), 1); @@ -1781,7 +1806,7 @@ mod failure_run_error_signatures { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.failure_runs.len(), 1); @@ -1806,7 +1831,7 @@ mod failure_run_error_signatures { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.failure_runs.len(), 1); @@ -1826,11 +1851,13 @@ mod compaction_loss_lost_work { let pricing = load_builtin_pricing(); let mut t0 = turn("s", "m0", 0); t0.ts = "2026-04-20T00:00:00.000Z".into(); - t0.tool_calls.push(tc_edit("u0", "Edit", "h0", "/src/foo.ts", "a", "b")); + t0.tool_calls + .push(tc_edit("u0", "Edit", "h0", "/src/foo.ts", "a", "b")); t0.tool_calls.push(tc_("u1", "Bash", "h1")); let mut t1 = turn("s", "m1", 1); t1.ts = "2026-04-20T00:00:01.000Z".into(); - t1.tool_calls.push(tc_edit("u2", "Edit", "h2", "/src/bar.ts", "c", "d")); + t1.tool_calls + .push(tc_edit("u2", "Edit", "h2", "/src/bar.ts", "c", "d")); t1.tool_calls.push(tc_("u3", "Read", "h3")); t1.tool_calls.push(tc_("u4", "Bash", "h4")); let events = vec![CompactionEvent { @@ -1853,7 +1880,7 @@ mod compaction_loss_lost_work { compactions: Some(&events), content_by_session: Some(&content_by_session), user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.compactions.len(), 1); @@ -1894,10 +1921,7 @@ mod compaction_loss_lost_work { }, ]; let mut content_by_session: HashMap> = HashMap::new(); - content_by_session.insert( - "s".into(), - vec![tool_result_record("s", "m0", "u0", "x")], - ); + content_by_session.insert("s".into(), vec![tool_result_record("s", "m0", "u0", "x")]); let result = detect_patterns( &[t0, t1], &DetectPatternsOptions { @@ -1905,7 +1929,7 @@ mod compaction_loss_lost_work { compactions: Some(&events), content_by_session: Some(&content_by_session), user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.compactions.len(), 2); @@ -1941,7 +1965,7 @@ mod compaction_loss_lost_work { compactions: Some(&events), user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.compactions.len(), 1); @@ -1997,7 +2021,7 @@ mod edit_revert_sample_preview { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_reverts.len(), 1); @@ -2040,7 +2064,7 @@ mod edit_revert_sample_preview { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); let preview = result.edit_reverts[0].sample_preview.as_ref().unwrap(); @@ -2060,7 +2084,7 @@ mod edit_revert_sample_preview { compactions: None, user_turns_by_session: None, content_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_reverts.len(), 1); @@ -2083,7 +2107,7 @@ mod edit_revert_sample_preview { content_by_session: Some(&content_by_session), compactions: None, user_turns_by_session: None, - tool_result_events: None + tool_result_events: None, }, ); assert_eq!(result.edit_reverts.len(), 1); diff --git a/crates/relayburn-sdk/src/analyze/replacement_savings.rs b/crates/relayburn-sdk/src/analyze/replacement_savings.rs index 40a9187b..00ebb4c6 100644 --- a/crates/relayburn-sdk/src/analyze/replacement_savings.rs +++ b/crates/relayburn-sdk/src/analyze/replacement_savings.rs @@ -9,9 +9,9 @@ use std::collections::HashMap; +use crate::reader::{ToolCall, TurnRecord}; use indexmap::IndexMap; use phf::phf_map; -use crate::reader::{ToolCall, TurnRecord}; /// Average tokens (input + output) one vanilla call of each tool consumes. /// Numbers mirror `DEFAULT_REPLACED_TOOL_TOKEN_COST` in the TS implementation. @@ -71,11 +71,7 @@ pub struct ReplacementSavingsSummary { pub by_tool: IndexMap, } -fn lookup_cost( - name: &str, - overrides: Option<&HashMap>, - fallback: u32, -) -> u32 { +fn lookup_cost(name: &str, overrides: Option<&HashMap>, fallback: u32) -> u32 { if let Some(o) = overrides { if let Some(v) = o.get(name) { return *v; @@ -124,10 +120,7 @@ pub fn estimate_savings_for_tool_call( options: Option<&ReplacementSavingsOptions>, ) -> Option { let collapsed = call.collapsed_calls.unwrap_or(0); - let replaced: &[String] = call - .replaced_tools - .as_deref() - .unwrap_or(&[]); + let replaced: &[String] = call.replaced_tools.as_deref().unwrap_or(&[]); if collapsed == 0 && replaced.is_empty() { return None; } @@ -136,7 +129,11 @@ pub fn estimate_savings_for_tool_call( let avg = average_replaced_cost(replaced, overrides, fallback); // When `collapsedCalls` is missing but `replaces` is present, treat the // call as having replaced one of each named tool. Conservative floor. - let calls = if collapsed > 0 { collapsed } else { replaced.len() as u64 }; + let calls = if collapsed > 0 { + collapsed + } else { + replaced.len() as u64 + }; Some(ToolCallSavings { collapsed_calls: calls, replaced_tools: replaced.to_vec(), @@ -157,10 +154,7 @@ pub fn summarize_replacement_savings( summary.calls += 1; summary.collapsed_calls += est.collapsed_calls; summary.estimated_tokens_saved += est.estimated_tokens_saved; - let agg = summary - .by_tool - .entry(tc.name.clone()) - .or_default(); + let agg = summary.by_tool.entry(tc.name.clone()).or_default(); agg.calls += 1; agg.collapsed_calls += est.collapsed_calls; agg.estimated_tokens_saved += est.estimated_tokens_saved; @@ -246,7 +240,11 @@ mod tests { #[test] fn estimates_using_average_per_call_cost_across_replaced_tools() { - let tc = call_with("relaywash__Search", Some(vec!["Glob", "Grep", "Read"]), Some(9)); + let tc = call_with( + "relaywash__Search", + Some(vec!["Glob", "Grep", "Read"]), + Some(9), + ); let est = estimate_savings_for_tool_call(&tc, None).expect("est"); let avg = (DEFAULT_REPLACED_TOOL_TOKEN_COST.get("Glob").unwrap() + DEFAULT_REPLACED_TOOL_TOKEN_COST.get("Grep").unwrap() @@ -275,7 +273,11 @@ mod tests { fn aggregates_savings_across_many_turns_and_tool_names() { let turns = vec![ turn(vec![ - call_with("relaywash__Search", Some(vec!["Glob", "Grep", "Read"]), Some(9)), + call_with( + "relaywash__Search", + Some(vec!["Glob", "Grep", "Read"]), + Some(9), + ), call("Bash"), ]), turn(vec![call_with( diff --git a/crates/relayburn-sdk/src/analyze/span_tree.rs b/crates/relayburn-sdk/src/analyze/span_tree.rs index 42f24c20..aabdfd28 100644 --- a/crates/relayburn-sdk/src/analyze/span_tree.rs +++ b/crates/relayburn-sdk/src/analyze/span_tree.rs @@ -46,31 +46,28 @@ //! - `tokens.output` — completion output tokens for this span. //! - `tokens.cache_read` — cached-prefix tokens read for this span. //! - `tokens.cache_write` — sum of `cache_create_5m` + `cache_create_1h` -//! for this span; the 5m/1h split is harness- -//! specific and not exposed here. +//! for this span; the 5m/1h split is harness-specific and not exposed here. //! - `tokens.reasoning` — extended-thinking reasoning tokens. //! //! Identity / context (encoded as [`AttrValue::String`]): //! - `model` — model identifier the inference ran against. //! - `request_id` — upstream Claude `requestId` (or the fallback -//! key — see [`crate::reader::InferenceKeySource`]). +//! key — see [`crate::reader::InferenceKeySource`]). //! - `agent_id` — `` filename portion of a subagent -//! sidecar transcript. +//! sidecar transcript. //! - `tool_use_id` — id of a `tool_use` block; matches the -//! paired `tool_result` event's `tool_use_id`. +//! paired `tool_result` event's `tool_use_id`. //! - `cwd` — working directory active for the turn (when -//! recorded by the harness; absent today on the -//! `TurnRecord` shape but reserved here so -//! future captures need no new key). +//! recorded by the harness; absent today on the `TurnRecord` shape but +//! reserved here so future captures need no new key). //! - `mode` — harness mode (plan / accept-edits / etc.) — -//! reserved for the same reason as `cwd`. +//! reserved for the same reason as `cwd`. //! - `stop_reason` — kebab-case [`StopReason`] wire string on the -//! root span when the trailing assistant row -//! carried one. +//! root span when the trailing assistant row carried one. //! //! Flags (encoded as [`AttrValue::Bool`]): //! - `unattached` — `true` on `Subagent` spans whose sidecar -//! could not be paired to a parent `ToolUse`. +//! could not be paired to a parent `ToolUse`. //! //! # Status mapping //! @@ -340,9 +337,7 @@ impl SpanNode { /// Depth-first iterator yielding `&SpanNode` for `self` and every /// descendant. Useful for "sum scalars across the tree" projections. pub fn iter_dfs(&self) -> SpanDfsIter<'_> { - SpanDfsIter { - stack: vec![self], - } + SpanDfsIter { stack: vec![self] } } } @@ -541,10 +536,8 @@ mod tests { fn iter_dfs_visits_parent_before_children() { let mut root = SpanNode::new(SpanKind::Turn, "turn"); let mut a = SpanNode::new(SpanKind::Inference, "a"); - a.children - .push(SpanNode::new(SpanKind::ToolUse, "Bash")); - a.children - .push(SpanNode::new(SpanKind::ToolUse, "Read")); + a.children.push(SpanNode::new(SpanKind::ToolUse, "Bash")); + a.children.push(SpanNode::new(SpanKind::ToolUse, "Read")); let b = SpanNode::new(SpanKind::Inference, "b"); root.children.push(a); root.children.push(b); @@ -574,9 +567,6 @@ mod tests { #[test] fn span_status_is_error_helper() { assert!(!SpanStatus::Ok.is_error()); - assert!(SpanStatus::Error { - msg: "x".into() - } - .is_error()); + assert!(SpanStatus::Error { msg: "x".into() }.is_error()); } } diff --git a/crates/relayburn-sdk/src/analyze/subagent_tree.rs b/crates/relayburn-sdk/src/analyze/subagent_tree.rs index c7fc296c..8ff9a187 100644 --- a/crates/relayburn-sdk/src/analyze/subagent_tree.rs +++ b/crates/relayburn-sdk/src/analyze/subagent_tree.rs @@ -7,8 +7,8 @@ //! the primary substrate for newer ingests; the legacy path falls back to //! `TurnRecord.subagent` only. -use indexmap::{IndexMap, IndexSet}; use crate::reader::{RelationshipType, SessionRelationshipRecord, TurnRecord}; +use indexmap::{IndexMap, IndexSet}; use serde::{Deserialize, Serialize}; use crate::analyze::cost::cost_for_turn; @@ -138,7 +138,11 @@ fn build_session_tree( session_id.to_string(), MutableNode { depth: 0, - ..MutableNode::new(session_id.to_string(), "main".to_string(), RelationshipType::Root) + ..MutableNode::new( + session_id.to_string(), + "main".to_string(), + RelationshipType::Root, + ) }, ); models.insert(session_id.to_string(), IndexSet::new()); @@ -274,7 +278,12 @@ fn build_relationship_trees( for r in relationships { let id = canonical_id(&state, &relationship_node_id(r)); - ensure_node(&mut state, &id, &label_for_relationship(r), r.relationship_type); + ensure_node( + &mut state, + &id, + &label_for_relationship(r), + r.relationship_type, + ); apply_relationship_metadata(&mut state, &id, r); if r.relationship_type == RelationshipType::Root { continue; @@ -371,12 +380,7 @@ fn canonical_id(state: &GraphState, id: &str) -> String { .unwrap_or_else(|| id.to_string()) } -fn ensure_node( - state: &mut GraphState, - id: &str, - label: &str, - relationship_type: RelationshipType, -) { +fn ensure_node(state: &mut GraphState, id: &str, label: &str, relationship_type: RelationshipType) { if !state.node_by_id.contains_key(id) { state.node_by_id.insert( id.to_string(), @@ -782,7 +786,9 @@ pub fn aggregate_subagent_type_stats( inv.ty = ty; } inv.turns += 1; - inv.cost += cost_for_turn(t, opts.pricing).map(|c| c.total).unwrap_or(0.0); + inv.cost += cost_for_turn(t, opts.pricing) + .map(|c| c.total) + .unwrap_or(0.0); } let mut by_type: IndexMap> = IndexMap::new(); let mut totals_by_type: IndexMap = IndexMap::new(); @@ -922,15 +928,34 @@ mod tests { let pricing = load_builtin_pricing(); let session_id = "sess-1"; let turns = vec![ - make_turn(session_id, "m1", "claude-sonnet-4-6", 0, SourceKind::ClaudeCode, None), - make_turn(session_id, "m2", "claude-sonnet-4-6", 1, SourceKind::ClaudeCode, None), + make_turn( + session_id, + "m1", + "claude-sonnet-4-6", + 0, + SourceKind::ClaudeCode, + None, + ), + make_turn( + session_id, + "m2", + "claude-sonnet-4-6", + 1, + SourceKind::ClaudeCode, + None, + ), make_turn( session_id, "o1", "claude-haiku-4-5", 2, SourceKind::ClaudeCode, - Some(sub(Some("u-outer"), Some(session_id), Some("Explore"), Some("Research"))), + Some(sub( + Some("u-outer"), + Some(session_id), + Some("Explore"), + Some("Research"), + )), ), make_turn( session_id, @@ -938,7 +963,12 @@ mod tests { "claude-haiku-4-5", 3, SourceKind::ClaudeCode, - Some(sub(Some("u-outer"), Some(session_id), Some("Explore"), None)), + Some(sub( + Some("u-outer"), + Some(session_id), + Some("Explore"), + None, + )), ), make_turn( session_id, @@ -946,7 +976,12 @@ mod tests { "claude-haiku-4-5", 4, SourceKind::ClaudeCode, - Some(sub(Some("u-inner"), Some("u-outer"), Some("code-reviewer"), None)), + Some(sub( + Some("u-inner"), + Some("u-outer"), + Some("code-reviewer"), + None, + )), ), ]; @@ -985,7 +1020,14 @@ mod tests { let pricing = load_builtin_pricing(); let session_id = "sess-2"; let turns = vec![ - make_turn(session_id, "m1", "claude-sonnet-4-6", 0, SourceKind::ClaudeCode, None), + make_turn( + session_id, + "m1", + "claude-sonnet-4-6", + 0, + SourceKind::ClaudeCode, + None, + ), make_turn( session_id, "s1", @@ -1015,14 +1057,26 @@ mod tests { let pricing = load_builtin_pricing(); let session_id = "sess-graph"; let turns = vec![ - make_turn(session_id, "m1", "claude-sonnet-4-6", 0, SourceKind::ClaudeCode, None), + make_turn( + session_id, + "m1", + "claude-sonnet-4-6", + 0, + SourceKind::ClaudeCode, + None, + ), make_turn( session_id, "o1", "claude-haiku-4-5", 1, SourceKind::ClaudeCode, - Some(sub(Some("u-outer"), Some(session_id), Some("Explore"), Some("Research"))), + Some(sub( + Some("u-outer"), + Some(session_id), + Some("Explore"), + Some("Research"), + )), ), make_turn( session_id, @@ -1030,7 +1084,12 @@ mod tests { "claude-haiku-4-5", 2, SourceKind::ClaudeCode, - Some(sub(Some("u-inner"), Some("u-outer"), Some("code-reviewer"), None)), + Some(sub( + Some("u-inner"), + Some("u-outer"), + Some("code-reviewer"), + None, + )), ), ]; let relationships = vec![ @@ -1064,20 +1123,43 @@ mod tests { ]; let legacy_opts = BuildSubagentTreeOptions::new(&pricing); - let legacy = build_subagent_tree(&turns, &legacy_opts).get(session_id).unwrap().clone(); + let legacy = build_subagent_tree(&turns, &legacy_opts) + .get(session_id) + .unwrap() + .clone(); let graph_opts = BuildSubagentTreeOptions::new(&pricing).with_relationships(&relationships); - let graph = build_subagent_tree(&turns, &graph_opts).get(session_id).unwrap().clone(); + let graph = build_subagent_tree(&turns, &graph_opts) + .get(session_id) + .unwrap() + .clone(); assert_eq!(graph, legacy); assert_eq!(graph.relationship_type, RelationshipType::Root); - assert_eq!(graph.children[0].relationship_type, RelationshipType::Subagent); + assert_eq!( + graph.children[0].relationship_type, + RelationshipType::Subagent + ); } #[test] fn joins_child_session_relationship_rows_to_turns_without_per_turn_subagent_metadata() { let pricing = load_builtin_pricing(); let turns = vec![ - make_turn("parent-session", "parent-1", "gpt-5.1-codex", 0, SourceKind::Codex, None), - make_turn("child-session", "child-1", "gpt-5.1-codex", 0, SourceKind::Codex, None), + make_turn( + "parent-session", + "parent-1", + "gpt-5.1-codex", + 0, + SourceKind::Codex, + None, + ), + make_turn( + "child-session", + "child-1", + "gpt-5.1-codex", + 0, + SourceKind::Codex, + None, + ), ]; let relationships = vec![ rel( @@ -1101,18 +1183,25 @@ mod tests { ]; let opts = BuildSubagentTreeOptions::new(&pricing).with_relationships(&relationships); - let root = build_subagent_tree(&turns, &opts).get("parent-session").unwrap().clone(); + let root = build_subagent_tree(&turns, &opts) + .get("parent-session") + .unwrap() + .clone(); assert_eq!(root.self_turns, 1); assert_eq!(root.cumulative_turns, 2); assert_eq!(root.children.len(), 1); assert_eq!(root.children[0].label, "worker"); assert_eq!(root.children[0].node_id, "child-session"); - assert_eq!(root.children[0].relationship_type, RelationshipType::Subagent); + assert_eq!( + root.children[0].relationship_type, + RelationshipType::Subagent + ); assert_eq!(root.children[0].self_turns, 1); } #[test] - fn does_not_alias_native_sidechain_session_roots_onto_agent_ids_when_turns_lack_subagent_fields() { + fn does_not_alias_native_sidechain_session_roots_onto_agent_ids_when_turns_lack_subagent_fields( + ) { let pricing = load_builtin_pricing(); let session_id = "partial-claude"; let turns = vec![make_turn( @@ -1144,7 +1233,10 @@ mod tests { ), ]; let opts = BuildSubagentTreeOptions::new(&pricing).with_relationships(&relationships); - let root = build_subagent_tree(&turns, &opts).get(session_id).unwrap().clone(); + let root = build_subagent_tree(&turns, &opts) + .get(session_id) + .unwrap() + .clone(); assert_eq!(root.node_id, session_id); assert_eq!(root.label, "main"); assert_eq!(root.self_turns, 1); @@ -1188,11 +1280,13 @@ mod tests { assert!(explore.p95_cost >= explore.median_cost); assert!((explore.mean_cost - explore.total_cost / 3.0).abs() < 1e-12); - let rev = stats.iter().find(|s| s.subagent_type == "code-reviewer").unwrap(); + let rev = stats + .iter() + .find(|s| s.subagent_type == "code-reviewer") + .unwrap(); assert_eq!(rev.invocations, 1); assert_eq!(rev.turns, 1); assert!((rev.median_cost - rev.total_cost).abs() < 1e-12); assert!((rev.p95_cost - rev.total_cost).abs() < 1e-12); } - } diff --git a/crates/relayburn-sdk/src/analyze/tool_call_patterns.rs b/crates/relayburn-sdk/src/analyze/tool_call_patterns.rs index f7902624..a22dfc89 100644 --- a/crates/relayburn-sdk/src/analyze/tool_call_patterns.rs +++ b/crates/relayburn-sdk/src/analyze/tool_call_patterns.rs @@ -9,16 +9,14 @@ use std::collections::{BTreeMap, HashSet}; -use phf::phf_set; use crate::reader::{ normalize_tool_name, parse_bash_command, BashParse, SourceKind, ToolCall, TurnRecord, }; +use phf::phf_set; use serde::{Deserialize, Serialize}; use crate::analyze::cost::lookup_model_rate; -use crate::analyze::findings::{ - severity_from_usd, EstimatedSavings, WasteAction, WasteFinding, -}; +use crate::analyze::findings::{severity_from_usd, EstimatedSavings, WasteAction, WasteFinding}; use crate::analyze::pricing::PricingTable; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -334,7 +332,10 @@ fn matches_git_state(parsed: &BashParse) -> bool { if parsed.binary != "git" { return false; } - matches!(parsed.subcommand.as_deref(), Some("status" | "diff" | "log")) + matches!( + parsed.subcommand.as_deref(), + Some("status" | "diff" | "log") + ) } fn matches_test_run(parsed: &BashParse) -> bool { @@ -478,7 +479,12 @@ pub fn tool_call_pattern_to_finding(finding: &ToolCallPatternFinding) -> WasteFi let evidence_str = if finding.evidence.is_empty() { String::new() } else { - let head: Vec<&str> = finding.evidence.iter().take(3).map(|s| s.as_str()).collect(); + let head: Vec<&str> = finding + .evidence + .iter() + .take(3) + .map(|s| s.as_str()) + .collect(); let extra = finding.evidence.len().saturating_sub(3); let tail = if extra > 0 { format!(", +{extra} more") @@ -604,10 +610,8 @@ mod tests { ], )); } - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let search = out .iter() .find(|f| f.category == ToolCallPatternCategory::SearchSequence) @@ -644,10 +648,8 @@ mod tests { ], ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); assert!(out .iter() .all(|f| f.category != ToolCallPatternCategory::SearchSequence)); @@ -670,10 +672,8 @@ mod tests { ], )); } - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); assert!(out .iter() .all(|f| f.category != ToolCallPatternCategory::SearchSequence)); @@ -692,10 +692,8 @@ mod tests { vec![tc(&format!("e{i}"), "Edit", Some("/src/foo.ts"))], )); } - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let cluster = out .iter() .find(|f| f.category == ToolCallPatternCategory::EditCluster) @@ -730,10 +728,8 @@ mod tests { vec![tc("e2", "Edit", Some("/f.ts"))], ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); assert!(out .iter() .all(|f| f.category != ToolCallPatternCategory::EditCluster)); @@ -743,15 +739,37 @@ mod tests { fn caps_window_at_exactly_5_consecutive_turn_indexes() { let pricing = pricing(); let turns = vec![ - turn("s", "m0", 0, SourceKind::ClaudeCode, vec![tc("e0", "Edit", Some("/f.ts"))]), - turn("s", "m1", 1, SourceKind::ClaudeCode, vec![tc("e1", "Edit", Some("/f.ts"))]), - turn("s", "m4", 4, SourceKind::ClaudeCode, vec![tc("e4", "Edit", Some("/f.ts"))]), - turn("s", "m5", 5, SourceKind::ClaudeCode, vec![tc("e5", "Edit", Some("/f.ts"))]), + turn( + "s", + "m0", + 0, + SourceKind::ClaudeCode, + vec![tc("e0", "Edit", Some("/f.ts"))], + ), + turn( + "s", + "m1", + 1, + SourceKind::ClaudeCode, + vec![tc("e1", "Edit", Some("/f.ts"))], + ), + turn( + "s", + "m4", + 4, + SourceKind::ClaudeCode, + vec![tc("e4", "Edit", Some("/f.ts"))], + ), + turn( + "s", + "m5", + 5, + SourceKind::ClaudeCode, + vec![tc("e5", "Edit", Some("/f.ts"))], + ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let cluster = out .iter() .find(|f| f.category == ToolCallPatternCategory::EditCluster) @@ -779,19 +797,14 @@ mod tests { vec![tc(&format!("b{i}"), "Edit", Some("/b.ts"))], )); } - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let clusters: Vec<_> = out .iter() .filter(|f| f.category == ToolCallPatternCategory::EditCluster) .collect(); assert_eq!(clusters.len(), 2); - let mut files: Vec<&str> = clusters - .iter() - .map(|c| c.evidence[0].as_str()) - .collect(); + let mut files: Vec<&str> = clusters.iter().map(|c| c.evidence[0].as_str()).collect(); files.sort(); assert_eq!(files, vec!["/a.ts", "/b.ts"]); } @@ -800,14 +813,30 @@ mod tests { fn flags_git_state_calls() { let pricing = pricing(); let turns = vec![ - turn("s", "m0", 0, SourceKind::ClaudeCode, vec![tc("a", "Bash", Some("git status"))]), - turn("s", "m1", 1, SourceKind::ClaudeCode, vec![tc("b", "Bash", Some("git diff HEAD~1"))]), - turn("s", "m2", 2, SourceKind::ClaudeCode, vec![tc("c", "Bash", Some("git log --oneline -n 5"))]), + turn( + "s", + "m0", + 0, + SourceKind::ClaudeCode, + vec![tc("a", "Bash", Some("git status"))], + ), + turn( + "s", + "m1", + 1, + SourceKind::ClaudeCode, + vec![tc("b", "Bash", Some("git diff HEAD~1"))], + ), + turn( + "s", + "m2", + 2, + SourceKind::ClaudeCode, + vec![tc("c", "Bash", Some("git log --oneline -n 5"))], + ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let git = out .iter() .find(|f| f.category == ToolCallPatternCategory::BashGitState) @@ -822,14 +851,30 @@ mod tests { fn flags_test_run_calls() { let pricing = pricing(); let turns = vec![ - turn("s", "m0", 0, SourceKind::ClaudeCode, vec![tc("a", "Bash", Some("pnpm test"))]), - turn("s", "m1", 1, SourceKind::ClaudeCode, vec![tc("b", "Bash", Some("pytest -k foo"))]), - turn("s", "m2", 2, SourceKind::ClaudeCode, vec![tc("c", "Bash", Some("jest --watch"))]), + turn( + "s", + "m0", + 0, + SourceKind::ClaudeCode, + vec![tc("a", "Bash", Some("pnpm test"))], + ), + turn( + "s", + "m1", + 1, + SourceKind::ClaudeCode, + vec![tc("b", "Bash", Some("pytest -k foo"))], + ), + turn( + "s", + "m2", + 2, + SourceKind::ClaudeCode, + vec![tc("c", "Bash", Some("jest --watch"))], + ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let test = out .iter() .find(|f| f.category == ToolCallPatternCategory::BashTestRun) @@ -841,13 +886,27 @@ mod tests { fn flags_gh_pr_and_gh_api_calls() { let pricing = pricing(); let turns = vec![ - turn("s", "m0", 0, SourceKind::ClaudeCode, vec![tc("a", "Bash", Some("gh pr view 123"))]), - turn("s", "m1", 1, SourceKind::ClaudeCode, vec![tc("b", "Bash", Some("gh api repos/foo/bar/pulls/1/comments"))]), + turn( + "s", + "m0", + 0, + SourceKind::ClaudeCode, + vec![tc("a", "Bash", Some("gh pr view 123"))], + ), + turn( + "s", + "m1", + 1, + SourceKind::ClaudeCode, + vec![tc( + "b", + "Bash", + Some("gh api repos/foo/bar/pulls/1/comments"), + )], + ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let gh = out .iter() .find(|f| f.category == ToolCallPatternCategory::BashGhPr) @@ -859,13 +918,23 @@ mod tests { fn does_not_match_gh_project_or_gh_prerelease() { let pricing = pricing(); let turns = vec![ - turn("s", "m0", 0, SourceKind::ClaudeCode, vec![tc("a", "Bash", Some("gh project list"))]), - turn("s", "m1", 1, SourceKind::ClaudeCode, vec![tc("b", "Bash", Some("gh project view 5"))]), + turn( + "s", + "m0", + 0, + SourceKind::ClaudeCode, + vec![tc("a", "Bash", Some("gh project list"))], + ), + turn( + "s", + "m1", + 1, + SourceKind::ClaudeCode, + vec![tc("b", "Bash", Some("gh project view 5"))], + ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); assert!(out .iter() .all(|f| f.category != ToolCallPatternCategory::BashGhPr)); @@ -875,13 +944,23 @@ mod tests { fn does_not_match_unrelated_bash_commands() { let pricing = pricing(); let turns = vec![ - turn("s", "m0", 0, SourceKind::ClaudeCode, vec![tc("a", "Bash", Some("ls -la"))]), - turn("s", "m1", 1, SourceKind::ClaudeCode, vec![tc("b", "Bash", Some("cat README.md"))]), + turn( + "s", + "m0", + 0, + SourceKind::ClaudeCode, + vec![tc("a", "Bash", Some("ls -la"))], + ), + turn( + "s", + "m1", + 1, + SourceKind::ClaudeCode, + vec![tc("b", "Bash", Some("cat README.md"))], + ), ]; - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); assert!(out.is_empty()); } @@ -902,10 +981,8 @@ mod tests { ], )); } - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let search = out .iter() .find(|f| f.category == ToolCallPatternCategory::SearchSequence) @@ -926,10 +1003,8 @@ mod tests { vec![tc(&format!("e{i}"), "apply_patch", Some("/src/x.ts"))], )); } - let out = detect_tool_call_patterns( - &turns, - &DetectToolCallPatternsOptions { pricing: &pricing }, - ); + let out = + detect_tool_call_patterns(&turns, &DetectToolCallPatternsOptions { pricing: &pricing }); let cluster = out .iter() .find(|f| f.category == ToolCallPatternCategory::EditCluster) diff --git a/crates/relayburn-sdk/src/analyze/tool_output_bloat.rs b/crates/relayburn-sdk/src/analyze/tool_output_bloat.rs index 75fe2fa1..03218bc3 100644 --- a/crates/relayburn-sdk/src/analyze/tool_output_bloat.rs +++ b/crates/relayburn-sdk/src/analyze/tool_output_bloat.rs @@ -493,10 +493,12 @@ impl<'a> DetectToolOutputBloatOptions<'a> { pub fn detect_tool_output_bloat(opts: &DetectToolOutputBloatOptions<'_>) -> Vec { let mut out: Vec = Vec::new(); if !opts.settings.is_empty() { - out.extend(detect_static_config_bloat(&DetectStaticConfigBloatOptions { - threshold: opts.threshold, - settings: opts.settings.to_vec(), - })); + out.extend(detect_static_config_bloat( + &DetectStaticConfigBloatOptions { + threshold: opts.threshold, + settings: opts.settings.to_vec(), + }, + )); } if !opts.tool_result_events.is_empty() { out.extend(detect_observed_bloat(&DetectObservedBloatOptions { @@ -636,9 +638,7 @@ Estimated next-turn carry cost {usd}. {advice}", mod tests { use super::*; use crate::analyze::pricing::load_builtin_pricing; - use crate::reader::{ - ToolCall, ToolResultEventSource, ToolResultStatus, Usage, UserTurnBlock, - }; + use crate::reader::{ToolCall, ToolResultEventSource, ToolResultStatus, Usage, UserTurnBlock}; use serde_json::json; use std::path::PathBuf; use tempfile::tempdir; @@ -689,6 +689,7 @@ mod tests { } } + #[allow(clippy::too_many_arguments)] fn evt_with( source: SourceKind, session_id: &str, @@ -724,6 +725,7 @@ mod tests { } } + #[allow(clippy::too_many_arguments)] fn user_turn_with( source: SourceKind, session_id: &str, @@ -827,7 +829,10 @@ mod tests { assert_eq!(f.evidenced_max_output, 20_000); assert_eq!(f.occurrence_count, 1); assert_eq!(f.cost, 0.0); - assert_eq!(f.evidence, vec!["/home/u/.claude/settings.json".to_string()]); + assert_eq!( + f.evidence, + vec!["/home/u/.claude/settings.json".to_string()] + ); } #[test] @@ -902,7 +907,10 @@ mod tests { settings, }); assert_eq!(out.len(), 1); - assert_eq!(out[0].evidence, vec!["/cwd/.claude/settings.json".to_string()]); + assert_eq!( + out[0].evidence, + vec!["/cwd/.claude/settings.json".to_string()] + ); assert_eq!(out[0].configured_limit, Some(99_999)); } @@ -1065,14 +1073,59 @@ mod tests { evt("s3", "tu_c", 0, Some("m3")), ]; let user_turns = vec![ - user_turn_with(SourceKind::ClaudeCode, "s1", "u1", "m1", "m2", "tu_a", 80_000, 20_000), - user_turn_with(SourceKind::ClaudeCode, "s2", "u2", "m2", "m3", "tu_b", 100_000, 25_000), - user_turn_with(SourceKind::ClaudeCode, "s3", "u3", "m3", "m4", "tu_c", 120_000, 30_000), + user_turn_with( + SourceKind::ClaudeCode, + "s1", + "u1", + "m1", + "m2", + "tu_a", + 80_000, + 20_000, + ), + user_turn_with( + SourceKind::ClaudeCode, + "s2", + "u2", + "m2", + "m3", + "tu_b", + 100_000, + 25_000, + ), + user_turn_with( + SourceKind::ClaudeCode, + "s3", + "u3", + "m3", + "m4", + "tu_c", + 120_000, + 30_000, + ), ]; let turns = vec![ - turn_with(SourceKind::ClaudeCode, "s1", "m1", 0, vec![tc("tu_a", "Bash")]), - turn_with(SourceKind::ClaudeCode, "s2", "m2", 0, vec![tc("tu_b", "Bash")]), - turn_with(SourceKind::ClaudeCode, "s3", "m3", 0, vec![tc("tu_c", "Bash")]), + turn_with( + SourceKind::ClaudeCode, + "s1", + "m1", + 0, + vec![tc("tu_a", "Bash")], + ), + turn_with( + SourceKind::ClaudeCode, + "s2", + "m2", + 0, + vec![tc("tu_b", "Bash")], + ), + turn_with( + SourceKind::ClaudeCode, + "s3", + "m3", + 0, + vec![tc("tu_c", "Bash")], + ), ]; let out = detect_observed_bloat(&DetectObservedBloatOptions { tool_result_events: &events, @@ -1125,14 +1178,59 @@ mod tests { ), ]; let user_turns = vec![ - user_turn_with(SourceKind::ClaudeCode, "s1", "u1", "m1", "m2", "tu_a", 80_000, 20_000), - user_turn_with(SourceKind::Codex, "s2", "u2", "m2", "m3", "call_b", 90_000, 22_500), - user_turn_with(SourceKind::Opencode, "s3", "u3", "m3", "m4", "opc_c", 85_000, 21_250), + user_turn_with( + SourceKind::ClaudeCode, + "s1", + "u1", + "m1", + "m2", + "tu_a", + 80_000, + 20_000, + ), + user_turn_with( + SourceKind::Codex, + "s2", + "u2", + "m2", + "m3", + "call_b", + 90_000, + 22_500, + ), + user_turn_with( + SourceKind::Opencode, + "s3", + "u3", + "m3", + "m4", + "opc_c", + 85_000, + 21_250, + ), ]; let turns = vec![ - turn_with(SourceKind::ClaudeCode, "s1", "m1", 0, vec![tc("tu_a", "Bash")]), - turn_with(SourceKind::Codex, "s2", "m2", 0, vec![tc("call_b", "shell")]), - turn_with(SourceKind::Opencode, "s3", "m3", 0, vec![tc("opc_c", "bash")]), + turn_with( + SourceKind::ClaudeCode, + "s1", + "m1", + 0, + vec![tc("tu_a", "Bash")], + ), + turn_with( + SourceKind::Codex, + "s2", + "m2", + 0, + vec![tc("call_b", "shell")], + ), + turn_with( + SourceKind::Opencode, + "s3", + "m3", + 0, + vec![tc("opc_c", "bash")], + ), ]; let out = detect_observed_bloat(&DetectObservedBloatOptions { tool_result_events: &events, @@ -1152,7 +1250,11 @@ mod tests { }); assert_eq!( sources, - vec![SourceKind::ClaudeCode, SourceKind::Codex, SourceKind::Opencode] + vec![ + SourceKind::ClaudeCode, + SourceKind::Codex, + SourceKind::Opencode + ] ); for b in &out { assert_eq!(b.tool_name, "Bash"); @@ -1435,7 +1537,10 @@ mod tests { WasteAction::Paste { label, text } => { assert!(label.contains("settings.json"), "label: {label}"); assert!(text.contains(BASH_MAX_OUTPUT_ENV_KEY), "text: {text}"); - assert!(text.contains("\"60000\""), "text should target 60000 chars: {text}"); + assert!( + text.contains("\"60000\""), + "text should target 60000 chars: {text}" + ); } other => panic!("expected Paste action, got {other:?}"), } @@ -1488,7 +1593,10 @@ mod tests { }); assert_eq!(result.len(), 1); assert_eq!(result[0].configured_limit, Some(80_000)); - assert_eq!(result[0].evidence, vec![path.to_string_lossy().into_owned()]); + assert_eq!( + result[0].evidence, + vec![path.to_string_lossy().into_owned()] + ); } #[test] diff --git a/crates/relayburn-sdk/src/export_verbs.rs b/crates/relayburn-sdk/src/export_verbs.rs index a2fdb7b6..6c5b3280 100644 --- a/crates/relayburn-sdk/src/export_verbs.rs +++ b/crates/relayburn-sdk/src/export_verbs.rs @@ -17,7 +17,7 @@ use crate::{Ledger, LedgerHandle, LedgerOpenOptions, SearchHit, SearchOptions}; /// Options for the FTS5 search verb. Equivalent to /// [`crate::ledger::SearchOptions`] but owns its strings so callers can /// build it without juggling lifetimes. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct SearchQueryOptions { /// FTS5 query string. Supports phrase (`"out of memory"`), boolean /// (`a OR b`), and prefix (`mem*`) syntax — see the SQLite docs. @@ -32,17 +32,6 @@ pub struct SearchQueryOptions { pub ledger_home: Option, } -impl Default for SearchQueryOptions { - fn default() -> Self { - Self { - query: String::new(), - limit: None, - session_id: None, - ledger_home: None, - } - } -} - impl SearchQueryOptions { /// Convenience constructor matching the lower-crate /// [`SearchOptions::new`] shape. @@ -223,14 +212,16 @@ mod tests { handle .raw_mut() .append_content(&[ - make_content("ses_a", "m1", "the build failed with an out of memory error"), + make_content( + "ses_a", + "m1", + "the build failed with an out of memory error", + ), make_content("ses_a", "m2", "permission denied while reading file"), ]) .unwrap(); - let result = handle - .search(SearchQueryOptions::new("memory")) - .unwrap(); + let result = handle.search(SearchQueryOptions::new("memory")).unwrap(); assert_eq!(result.query, "memory"); assert_eq!(result.hits.len(), 1); assert_eq!(result.hits[0].session_id, "ses_a"); diff --git a/crates/relayburn-sdk/src/ingest.rs b/crates/relayburn-sdk/src/ingest.rs index a0266b26..5bd72f08 100644 --- a/crates/relayburn-sdk/src/ingest.rs +++ b/crates/relayburn-sdk/src/ingest.rs @@ -21,6 +21,7 @@ pub mod cursors; pub(crate) mod fs_events; pub mod gap; +#[allow(clippy::module_inception)] pub mod ingest; pub mod pending_stamps; pub mod reingest; diff --git a/crates/relayburn-sdk/src/ingest/fs_events.rs b/crates/relayburn-sdk/src/ingest/fs_events.rs index f5186689..ad93deaf 100644 --- a/crates/relayburn-sdk/src/ingest/fs_events.rs +++ b/crates/relayburn-sdk/src/ingest/fs_events.rs @@ -199,11 +199,9 @@ mod tests { // period, the second call would hang indefinitely. let debounce = Duration::from_millis(50); for _ in 0..3 { - let result = tokio::time::timeout( - Duration::from_millis(500), - burst.wait_for_burst(debounce), - ) - .await; + let result = + tokio::time::timeout(Duration::from_millis(500), burst.wait_for_burst(debounce)) + .await; assert!(matches!(result, Ok(Some(())))); } diff --git a/crates/relayburn-sdk/src/ingest/gap.rs b/crates/relayburn-sdk/src/ingest/gap.rs index 42c86016..90799ce5 100644 --- a/crates/relayburn-sdk/src/ingest/gap.rs +++ b/crates/relayburn-sdk/src/ingest/gap.rs @@ -241,10 +241,7 @@ pub struct ToolCallGapCounts { /// /// Mirrors the TS `countToolCallGaps`. #[cfg(test)] -pub fn count_tool_call_gaps( - turns: &[TurnRecord], - content: &[ContentRecord], -) -> ToolCallGapCounts { +pub fn count_tool_call_gaps(turns: &[TurnRecord], content: &[ContentRecord]) -> ToolCallGapCounts { let tool_calls_observed: u64 = turns.iter().map(|t| t.tool_calls.len() as u64).sum(); if tool_calls_observed == 0 { return ToolCallGapCounts { @@ -431,7 +428,12 @@ mod tests { } } - fn make_content(session: &str, message: &str, kind: ContentKind, role: ContentRole) -> ContentRecord { + fn make_content( + session: &str, + message: &str, + kind: ContentKind, + role: ContentRole, + ) -> ContentRecord { ContentRecord { v: 1, source: SourceKind::Codex, @@ -454,7 +456,12 @@ mod tests { ]; let content = vec![ make_content("sess_test", "m1", ContentKind::Text, ContentRole::Assistant), - make_content("sess_test", "m1", ContentKind::ToolUse, ContentRole::Assistant), + make_content( + "sess_test", + "m1", + ContentKind::ToolUse, + ContentRole::Assistant, + ), ]; let r = count_tool_call_gaps(&turns, &content); assert!(r.session_affected); @@ -477,7 +484,12 @@ mod tests { fn count_tool_call_gaps_with_tool_result_does_not_flag() { let turns = vec![make_turn("sess_test", "m1", 1)]; let content = vec![ - make_content("sess_test", "m1", ContentKind::ToolUse, ContentRole::Assistant), + make_content( + "sess_test", + "m1", + ContentKind::ToolUse, + ContentRole::Assistant, + ), make_content( "sess_test", "m1", @@ -574,7 +586,11 @@ mod tests { // Pass 2: same session heals — affected set is now empty. record_session_gap(AdapterName::Codex, "sess_gap_1", 0, 1); emit_gap_warning(AdapterName::Codex, ContentStoreMode::Full, None); - assert_eq!(captured.lock().unwrap().len(), 1, "no new warning after heal"); + assert_eq!( + captured.lock().unwrap().len(), + 1, + "no new warning after heal" + ); // Pass 3: a brand-new gap session re-ignites the warning, // proving the suppression marker decayed back to zero (not diff --git a/crates/relayburn-sdk/src/ingest/gap_warning_tests.rs b/crates/relayburn-sdk/src/ingest/gap_warning_tests.rs index 01546b19..664ad5d3 100644 --- a/crates/relayburn-sdk/src/ingest/gap_warning_tests.rs +++ b/crates/relayburn-sdk/src/ingest/gap_warning_tests.rs @@ -300,11 +300,7 @@ async fn no_gap_warning_for_chat_only_claude_session() { "cwd": "/tmp/project", "sessionId": sid, }); - fs::write( - &session_file, - format!("{}\n{}\n", user, assistant), - ) - .unwrap(); + fs::write(&session_file, format!("{}\n{}\n", user, assistant)).unwrap(); let warnings = with_captured_gap_warnings(|captured| async move { let mut ledger = open_ledger_in(&tmp); @@ -621,11 +617,7 @@ async fn per_harness_then_ingest_all_keeps_each_adapter_isolated() { // Step 2: drop a fresh claude gap fixture; a follow-up // ingest_all should stay silent for codex (nothing new) but // emit for claude (fresh adapter). - let project_dir = roots - .claude_projects_dir - .as_ref() - .unwrap() - .join("-tmp-mix"); + let project_dir = roots.claude_projects_dir.as_ref().unwrap().join("-tmp-mix"); fs::create_dir_all(&project_dir).unwrap(); let claude_sid = "mmmmmmmm-mmmm-mmmm-mmmm-mmmmmmmmmmmm"; fs::write( @@ -649,10 +641,7 @@ async fn per_harness_then_ingest_all_keeps_each_adapter_isolated() { .iter() .filter(|w| w.starts_with("claude:")) .count(); - let codex_count = after_all - .iter() - .filter(|w| w.starts_with("codex:")) - .count(); + let codex_count = after_all.iter().filter(|w| w.starts_with("codex:")).count(); assert_eq!( claude_count, 1, "ingest_all should fire one claude warning after fresh fixture (got {:?})", diff --git a/crates/relayburn-sdk/src/ingest/ingest.rs b/crates/relayburn-sdk/src/ingest/ingest.rs index 34de80a6..6c7d3189 100644 --- a/crates/relayburn-sdk/src/ingest/ingest.rs +++ b/crates/relayburn-sdk/src/ingest/ingest.rs @@ -382,12 +382,7 @@ pub fn ingest_claude_transcript_path( session_mtime_ms: Some(mtime_ms(&pre_cursor_meta)), cwd, }; - resolve_pending_stamps_for_report( - ledger, - &candidate, - opts.ledger_home.as_deref(), - &mut report, - ); + resolve_pending_stamps_for_report(ledger, &candidate, opts.ledger_home.as_deref(), &mut report); // Re-stat after parsing so the cursor reflects the byte position the // parser actually read to. `parse_claude_session` uses BufReader::lines() @@ -1019,9 +1014,7 @@ macro_rules! impl_derived_records_common { fn turns(&self) -> &[TurnRecord] { &self.turns } - fn request_id_lookup( - &self, - ) -> std::borrow::Cow<'_, crate::reader::RequestIdLookup> { + fn request_id_lookup(&self) -> std::borrow::Cow<'_, crate::reader::RequestIdLookup> { // Default: empty lookup (Codex / opencode). Claude // overrides this in the per-impl block below; we can't // express that override via a single macro arm because diff --git a/crates/relayburn-sdk/src/ingest/pending_stamps.rs b/crates/relayburn-sdk/src/ingest/pending_stamps.rs index ea1bc014..8465253b 100644 --- a/crates/relayburn-sdk/src/ingest/pending_stamps.rs +++ b/crates/relayburn-sdk/src/ingest/pending_stamps.rs @@ -428,11 +428,9 @@ fn format_iso_8601(t: SystemTime) -> String { .duration_since(UNIX_EPOCH) .unwrap_or(Duration::from_secs(0)); let nanos = dur.as_nanos() as i128; - let dt = - OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH); - let fmt = format_description!( - "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z" - ); + let dt = OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH); + let fmt = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"); dt.format(&fmt).expect("format ms iso") } diff --git a/crates/relayburn-sdk/src/ingest_verb.rs b/crates/relayburn-sdk/src/ingest_verb.rs index 19e5f60e..854af01b 100644 --- a/crates/relayburn-sdk/src/ingest_verb.rs +++ b/crates/relayburn-sdk/src/ingest_verb.rs @@ -12,7 +12,7 @@ use std::path::PathBuf; -use crate::ingest::{IngestOptions as RawIngestOptions, IngestReport, IngestRoots, ingest_all}; +use crate::ingest::{ingest_all, IngestOptions as RawIngestOptions, IngestReport, IngestRoots}; use crate::{Ledger, LedgerHandle, LedgerOpenOptions}; diff --git a/crates/relayburn-sdk/src/ledger.rs b/crates/relayburn-sdk/src/ledger.rs index 69915a63..8642d5fe 100644 --- a/crates/relayburn-sdk/src/ledger.rs +++ b/crates/relayburn-sdk/src/ledger.rs @@ -120,10 +120,7 @@ impl Ledger { /// existing row — inferences are pure derived state and a re-parse /// can legitimately produce updated `end_ts` / merged `usage` /// values. - pub fn append_inferences( - &mut self, - records: &[crate::reader::Inference], - ) -> Result { + pub fn append_inferences(&mut self, records: &[crate::reader::Inference]) -> Result { writer::append_inferences(&mut self.conns.burn, records) } @@ -468,17 +465,16 @@ impl Ledger { pub fn count_reset_targets(&self) -> Result { let mut rows_dropped = 0i64; for table in DERIVABLE_TABLES { - let count: i64 = self.conns.burn.query_row( - &format!("SELECT COUNT(*) FROM {table}"), - [], - |r| r.get(0), - )?; + let count: i64 = + self.conns + .burn + .query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0))?; rows_dropped += count; } - let stamps_dropped: i64 = self - .conns - .burn - .query_row("SELECT COUNT(*) FROM stamps", [], |r| r.get(0))?; + let stamps_dropped: i64 = + self.conns + .burn + .query_row("SELECT COUNT(*) FROM stamps", [], |r| r.get(0))?; let content_rows_dropped = content::count_content(&self.conns.content)?; Ok(ResetSummary { rows_dropped: rows_dropped as usize, diff --git a/crates/relayburn-sdk/src/ledger/bootstrap.rs b/crates/relayburn-sdk/src/ledger/bootstrap.rs index 46c92118..c0e35425 100644 --- a/crates/relayburn-sdk/src/ledger/bootstrap.rs +++ b/crates/relayburn-sdk/src/ledger/bootstrap.rs @@ -126,10 +126,7 @@ pub(crate) fn decide_bootstrap(burn_path: &Path) -> BootstrapDecision { /// Apply the decision captured by [`decide_bootstrap`]. A no-op for /// `BootstrapDecision::Skip`; for `Rebuild`, wipes derivable tables /// and replays the JSONL via `writer::append_*`. -pub(crate) fn apply_bootstrap( - burn: &mut Connection, - decision: BootstrapDecision, -) -> Result<()> { +pub(crate) fn apply_bootstrap(burn: &mut Connection, decision: BootstrapDecision) -> Result<()> { match decision { BootstrapDecision::Skip => Ok(()), BootstrapDecision::Rebuild { jsonl_path } => rebuild_from_jsonl(burn, &jsonl_path), @@ -372,7 +369,10 @@ mod tests { } // Bump the sqlite's mtime explicitly so we don't depend on // filesystem resolution. - set_mtime(&burn, SystemTime::now() + std::time::Duration::from_secs(60)); + set_mtime( + &burn, + SystemTime::now() + std::time::Duration::from_secs(60), + ); // Append a second turn to the JSONL — but *force* its mtime to // be older than sqlite's. The reopen should NOT rebuild and the @@ -380,7 +380,10 @@ mod tests { let mut f = fs::OpenOptions::new().append(true).open(&jsonl).unwrap(); writeln!(f, "{}", turn_envelope_line("sess-a", "msg-2", 20)).unwrap(); drop(f); - set_mtime(&jsonl, SystemTime::now() - std::time::Duration::from_secs(60)); + set_mtime( + &jsonl, + SystemTime::now() - std::time::Duration::from_secs(60), + ); let l = Ledger::open(&burn, &content).unwrap(); assert_eq!(l.count_table("turns").unwrap(), 1); @@ -403,7 +406,10 @@ mod tests { } // Force sqlite's mtime well into the past. - set_mtime(&burn, SystemTime::now() - std::time::Duration::from_secs(3600)); + set_mtime( + &burn, + SystemTime::now() - std::time::Duration::from_secs(3600), + ); // Append to JSONL — its mtime is now newer than sqlite's. let mut f = fs::OpenOptions::new().append(true).open(&jsonl).unwrap(); diff --git a/crates/relayburn-sdk/src/ledger/config.rs b/crates/relayburn-sdk/src/ledger/config.rs index 924caa14..2b1fbd3c 100644 --- a/crates/relayburn-sdk/src/ledger/config.rs +++ b/crates/relayburn-sdk/src/ledger/config.rs @@ -173,11 +173,7 @@ fn read_config_file(path: &Path) -> Option { // Missing config file is the common case and not worth // mentioning — same fail-quiet behaviour as TS. if err.kind() != std::io::ErrorKind::NotFound { - eprintln!( - "[burn] warning: could not read {}: {}", - path.display(), - err - ); + eprintln!("[burn] warning: could not read {}: {}", path.display(), err); } return None; } @@ -357,11 +353,7 @@ mod tests { with_clean_env(|| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("config.json"); - std::fs::write( - &path, - r#"{"content":{"store":"full","retentionDays":-1}}"#, - ) - .unwrap(); + std::fs::write(&path, r#"{"content":{"store":"full","retentionDays":-1}}"#).unwrap(); let cfg = load_config_at(&path).unwrap(); assert_eq!(cfg.content.retention_days, Retention::Forever); }); @@ -417,11 +409,7 @@ mod tests { with_clean_env(|| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("config.json"); - std::fs::write( - &path, - r#"{"content":{"store":"full","retentionDays":1.5}}"#, - ) - .unwrap(); + std::fs::write(&path, r#"{"content":{"store":"full","retentionDays":1.5}}"#).unwrap(); let cfg = load_config_at(&path).unwrap(); assert_eq!(cfg.content.retention_days, Retention::Days(1.5)); }); @@ -454,11 +442,7 @@ mod tests { with_clean_env(|| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("config.json"); - std::fs::write( - &path, - r#"{"content":{"store":"bogus","retentionDays":7}}"#, - ) - .unwrap(); + std::fs::write(&path, r#"{"content":{"store":"bogus","retentionDays":7}}"#).unwrap(); let cfg = load_config_at(&path).unwrap(); assert_eq!(cfg.content.store, ContentStoreMode::Full); assert_eq!(cfg.content.retention_days, Retention::Days(7.0)); diff --git a/crates/relayburn-sdk/src/ledger/db.rs b/crates/relayburn-sdk/src/ledger/db.rs index 128ea069..38f2669e 100644 --- a/crates/relayburn-sdk/src/ledger/db.rs +++ b/crates/relayburn-sdk/src/ledger/db.rs @@ -51,8 +51,7 @@ impl Connections { // creates `burn.sqlite` as a side effect — if we waited, the // freshly-created (and newer-than-JSONL) sqlite mtime would // always look "current" and we'd skip the rebuild. - let bootstrap_decision = - crate::ledger::bootstrap::decide_bootstrap(burn_path); + let bootstrap_decision = crate::ledger::bootstrap::decide_bootstrap(burn_path); let mut burn = Connection::open(burn_path)?; configure_pragmas(&burn)?; diff --git a/crates/relayburn-sdk/src/ledger/paths.rs b/crates/relayburn-sdk/src/ledger/paths.rs index 3b2b56e2..09175f65 100644 --- a/crates/relayburn-sdk/src/ledger/paths.rs +++ b/crates/relayburn-sdk/src/ledger/paths.rs @@ -111,7 +111,10 @@ mod tests { env::set_var("HOME", "/tmp/burn-paths-test-home"); let p = ledger_home(); - assert_eq!(p, PathBuf::from("/tmp/burn-paths-test-home/.agentworkforce/burn")); + assert_eq!( + p, + PathBuf::from("/tmp/burn-paths-test-home/.agentworkforce/burn") + ); match prev_home { Some(v) => env::set_var("HOME", v), @@ -149,9 +152,7 @@ mod tests { #[test] fn accepts_real_session_ids() { - assert!(is_valid_session_id( - "0a1b2c3d-4e5f-6789-abcd-ef0123456789" - )); + assert!(is_valid_session_id("0a1b2c3d-4e5f-6789-abcd-ef0123456789")); assert!(is_valid_session_id("ses_abc123")); assert!(is_valid_session_id("sess_x")); assert!(is_valid_session_id("turn_42")); diff --git a/crates/relayburn-sdk/src/ledger/reader.rs b/crates/relayburn-sdk/src/ledger/reader.rs index dd18d25f..723fe5b7 100644 --- a/crates/relayburn-sdk/src/ledger/reader.rs +++ b/crates/relayburn-sdk/src/ledger/reader.rs @@ -8,7 +8,7 @@ use std::collections::{BTreeMap, HashSet}; -use rusqlite::{Connection, params_from_iter}; +use rusqlite::{params_from_iter, Connection}; use serde::{Deserialize, Serialize}; use crate::reader::{ @@ -19,7 +19,7 @@ use crate::reader::{ use crate::ledger::error::Result; use crate::ledger::paths::is_valid_session_id; use crate::ledger::query::Query; -use crate::ledger::stamp::{Enrichment, Stamp, StampSelector, stamp_matches}; +use crate::ledger::stamp::{stamp_matches, Enrichment, Stamp, StampSelector}; /// A turn with stamp enrichment folded in. Enrichment is a flat /// string→string map; entries from later stamps win. @@ -408,4 +408,3 @@ pub(crate) fn raw_record_jsons(conn: &Connection, table: &str) -> Result>>()?; Ok(rows) } - diff --git a/crates/relayburn-sdk/src/ledger/stamp.rs b/crates/relayburn-sdk/src/ledger/stamp.rs index c80c4e41..29924408 100644 --- a/crates/relayburn-sdk/src/ledger/stamp.rs +++ b/crates/relayburn-sdk/src/ledger/stamp.rs @@ -180,8 +180,14 @@ mod tests { Enrichment::new(), ) .unwrap(); - assert!(stamp_matches(&s, &make_turn("a", "m1", "2025-01-01T00:00:00Z"))); - assert!(!stamp_matches(&s, &make_turn("b", "m1", "2025-01-01T00:00:00Z"))); + assert!(stamp_matches( + &s, + &make_turn("a", "m1", "2025-01-01T00:00:00Z") + )); + assert!(!stamp_matches( + &s, + &make_turn("b", "m1", "2025-01-01T00:00:00Z") + )); } #[test] @@ -200,8 +206,14 @@ mod tests { Enrichment::new(), ) .unwrap(); - assert!(stamp_matches(&s, &make_turn("a", "m", "2025-01-01T00:00:00Z"))); - assert!(stamp_matches(&s, &make_turn("a", "m", "2025-01-02T00:00:00Z"))); + assert!(stamp_matches( + &s, + &make_turn("a", "m", "2025-01-01T00:00:00Z") + )); + assert!(stamp_matches( + &s, + &make_turn("a", "m", "2025-01-02T00:00:00Z") + )); assert!(!stamp_matches( &s, &make_turn("a", "m", "2024-12-31T23:59:59Z") diff --git a/crates/relayburn-sdk/src/ledger/tests.rs b/crates/relayburn-sdk/src/ledger/tests.rs index 47c7d02a..381901f6 100644 --- a/crates/relayburn-sdk/src/ledger/tests.rs +++ b/crates/relayburn-sdk/src/ledger/tests.rs @@ -239,7 +239,10 @@ fn stamps_survive_state_rebuild() { let stamps = l.list_stamps().unwrap(); assert_eq!(stamps.len(), 1); - assert_eq!(stamps[0].enrichment.get("role").map(String::as_str), Some("fix-bug")); + assert_eq!( + stamps[0].enrichment.get("role").map(String::as_str), + Some("fix-bug") + ); } #[test] @@ -248,7 +251,8 @@ fn cursors_survive_state_rebuild() { let tmp = TempDir::new().unwrap(); let mut l = open_in(&tmp); - l.write_cursors(r#"{"claude-code": "2025-01-01T00:00:00Z"}"#).unwrap(); + l.write_cursors(r#"{"claude-code": "2025-01-01T00:00:00Z"}"#) + .unwrap(); l.append_turns(&[make_turn("s1", "m1", "2025-01-01T00:00:00Z", 10)]) .unwrap(); l.rebuild_derivable().unwrap(); @@ -266,7 +270,8 @@ fn reset_wipes_derivable_stamps_content_and_cursors() { let tmp = TempDir::new().unwrap(); let mut l = open_in(&tmp); - l.write_cursors(r#"{"claude-code": "2025-01-01T00:00:00Z"}"#).unwrap(); + l.write_cursors(r#"{"claude-code": "2025-01-01T00:00:00Z"}"#) + .unwrap(); let mut enrichment = BTreeMap::new(); enrichment.insert("role".into(), "fix-bug".into()); @@ -332,7 +337,8 @@ fn count_reset_targets_does_not_mutate() { l.append_stamp(&stamp).unwrap(); l.append_turns(&[make_turn("s1", "m1", "2025-01-01T00:00:00Z", 10)]) .unwrap(); - l.append_content(&[make_content("s1", "m1", "hello")]).unwrap(); + l.append_content(&[make_content("s1", "m1", "hello")]) + .unwrap(); let preview = l.count_reset_targets().unwrap(); assert_eq!(preview.rows_dropped, 1); @@ -417,7 +423,11 @@ fn fts5_search_returns_ranked_snippets() { let tmp = TempDir::new().unwrap(); let mut l = open_in(&tmp); l.append_content(&[ - make_content("ses_a", "m1", "the build failed with an out of memory error"), + make_content( + "ses_a", + "m1", + "the build failed with an out of memory error", + ), make_content("ses_a", "m2", "permission denied while reading file"), make_content("ses_b", "m1", "out of memory: killed by oom-killer"), ]) @@ -465,7 +475,9 @@ fn fts5_index_stays_consistent_across_insert_delete() { l.append_content(&[make_content("ses_a", "m1", "needle in haystack")]) .unwrap(); assert_eq!( - l.search_content(SearchOptions::new("needle")).unwrap().len(), + l.search_content(SearchOptions::new("needle")) + .unwrap() + .len(), 1 ); @@ -474,7 +486,9 @@ fn fts5_index_stays_consistent_across_insert_delete() { assert_eq!(l.count_content().unwrap(), 0); // Trigger should have removed the FTS row too. assert_eq!( - l.search_content(SearchOptions::new("needle")).unwrap().len(), + l.search_content(SearchOptions::new("needle")) + .unwrap() + .len(), 0 ); @@ -482,7 +496,9 @@ fn fts5_index_stays_consistent_across_insert_delete() { l.append_content(&[make_content("ses_a", "m1", "needle in haystack")]) .unwrap(); assert_eq!( - l.search_content(SearchOptions::new("needle")).unwrap().len(), + l.search_content(SearchOptions::new("needle")) + .unwrap() + .len(), 1 ); } @@ -892,7 +908,10 @@ fn relationship_records_round_trip() { subagent_type: None, description: None, }; - assert_eq!(l.append_relationships(std::slice::from_ref(&rel)).unwrap(), 1); + assert_eq!( + l.append_relationships(std::slice::from_ref(&rel)).unwrap(), + 1 + ); // Re-append: dedup'd by primary-key fingerprint. assert_eq!(l.append_relationships(&[rel]).unwrap(), 0); assert_eq!(l.count_table("relationships").unwrap(), 1); @@ -1038,19 +1057,17 @@ fn pruning_content_does_not_lock_events_db() { // events DB; analytic queries on burn.sqlite keep running. let tmp = TempDir::new().unwrap(); let layout = LedgerLayout::under(tmp.path()); - let ledger = Arc::new(Mutex::new(Ledger::open(&layout.burn, &layout.content).unwrap())); + let ledger = Arc::new(Mutex::new( + Ledger::open(&layout.burn, &layout.content).unwrap(), + )); { let mut l = ledger.lock().unwrap(); l.append_turns(&[make_turn("s1", "m1", "2025-01-01T00:00:00Z", 10)]) .unwrap(); for i in 0..50 { - l.append_content(&[make_content( - "ses_x", - &format!("m{i}"), - "lots of body text", - )]) - .unwrap(); + l.append_content(&[make_content("ses_x", &format!("m{i}"), "lots of body text")]) + .unwrap(); } } @@ -1233,9 +1250,7 @@ fn query_relationships_session_filter_matches_either_endpoint() { }; l.append_relationships(&[parent_edge, unrelated]).unwrap(); - let by_child = l - .query_relationships(&Query::for_session("child")) - .unwrap(); + let by_child = l.query_relationships(&Query::for_session("child")).unwrap(); assert_eq!(by_child.len(), 1); let by_parent = l diff --git a/crates/relayburn-sdk/src/ledger/writer.rs b/crates/relayburn-sdk/src/ledger/writer.rs index 79096448..bd12f577 100644 --- a/crates/relayburn-sdk/src/ledger/writer.rs +++ b/crates/relayburn-sdk/src/ledger/writer.rs @@ -115,7 +115,8 @@ pub(crate) fn append_compactions( for e in events { let id = compaction_id_fingerprint(e); let json = serde_json::to_string(e)?; - let changed = insert.execute(params![id, e.source.wire_str(), e.session_id, e.ts, json])?; + let changed = + insert.execute(params![id, e.source.wire_str(), e.session_id, e.ts, json])?; if changed > 0 { appended += 1; } @@ -209,10 +210,7 @@ pub(crate) fn append_tool_result_events( /// if the JSONL grew between runs. The composite PK /// `(source, session_id, request_id)` is the natural identity. See issue /// #434. -pub(crate) fn append_inferences( - conn: &mut Connection, - records: &[Inference], -) -> Result { +pub(crate) fn append_inferences(conn: &mut Connection, records: &[Inference]) -> Result { if records.is_empty() { return Ok(0); } @@ -267,7 +265,12 @@ pub(crate) fn append_user_turns( let id = user_turn_id_fingerprint(r); let json = serde_json::to_string(r)?; let changed = insert.execute(params![ - id, r.source.wire_str(), r.session_id, r.user_uuid, r.ts, json, + id, + r.source.wire_str(), + r.session_id, + r.user_uuid, + r.ts, + json, ])?; if changed > 0 { appended += 1; @@ -326,10 +329,7 @@ pub(crate) fn append_stamp(conn: &mut Connection, stamp: &Stamp) -> Result<()> { Ok(()) } -pub(crate) fn append_content( - conn: &mut Connection, - records: &[ContentRecord], -) -> Result { +pub(crate) fn append_content(conn: &mut Connection, records: &[ContentRecord]) -> Result { if records.is_empty() { return Ok(0); } @@ -399,4 +399,3 @@ pub(crate) fn synthesize_relationship(stamp: &Stamp) -> Option String { now_lex_token() } - diff --git a/crates/relayburn-sdk/src/lib.rs b/crates/relayburn-sdk/src/lib.rs index b4061ad9..59fa6ba9 100644 --- a/crates/relayburn-sdk/src/lib.rs +++ b/crates/relayburn-sdk/src/lib.rs @@ -67,12 +67,12 @@ pub use stamp_verb::*; pub use crate::reader::{ build_claude_span_tree, build_codex_span_tree, build_inferences, count_subagents_under, - discover_subagents, pair_to_main as pair_subagents_to_main, parse_bash_command, resolve_project, - ActivityCategory, BashParse, ClassificationInput, ClassificationResult, ClaudeSpanTreeInputs, - CodexSpanTreeInputs, CompactionEvent, - ContentKind, ContentRecord, ContentRole, ContentStoreMode, ContentToolResult, ContentToolUse, - Coverage, Fidelity, FidelityClass, Harness, Inference, InferenceKeySource, InferenceKind, - ProjectResolver, RelationshipSourceKind, RelationshipType, RequestIdLookup, ResolvedProject, + discover_subagents, pair_to_main as pair_subagents_to_main, parse_bash_command, + resolve_project, ActivityCategory, BashParse, ClassificationInput, ClassificationResult, + ClaudeSpanTreeInputs, CodexSpanTreeInputs, CompactionEvent, ContentKind, ContentRecord, + ContentRole, ContentStoreMode, ContentToolResult, ContentToolUse, Coverage, Fidelity, + FidelityClass, Harness, Inference, InferenceKeySource, InferenceKind, ProjectResolver, + RelationshipSourceKind, RelationshipType, RequestIdLookup, ResolvedProject, SessionRelationshipRecord, SourceKind, StopReason, Subagent, SubagentCounts, SubagentTranscript, ToolCall, ToolResultEventRecord, ToolResultEventSource, ToolResultStatus, ToolUseRef, TurnKey, TurnRecord, Usage, UsageAttribution, UsageGranularity, UserTurnBlock, @@ -100,20 +100,20 @@ pub use crate::analyze::{ cost_for_usage, describe_applies_to, detect_patterns, detect_tool_call_patterns, detect_tool_output_bloat, filter_turns_by_provider, filter_turns_by_provider_with_rules, find_overhead_files, findings_from_patterns, has_minimum_fidelity, load_overhead_file, - load_pricing, provider_for, AsTurnLike, ProviderFilter, ProviderRule, - render_unified_diff_for_recommendation, sum_costs, summarize_fidelity, - summarize_replacement_savings, AggregateByProviderOptions, AttributeOverheadInput, - AttributionMethod, BashAggregation, BashVerbAggregation, BuildSubagentTreeOptions, - CompareCategory, CompareCell, CompareFromArchiveResult, + load_pricing, provider_for, render_unified_diff_for_recommendation, sum_costs, + summarize_fidelity, summarize_replacement_savings, AggregateByProviderOptions, AsTurnLike, + AttributeOverheadInput, AttributionMethod, BashAggregation, BashVerbAggregation, + BuildSubagentTreeOptions, CompareCategory, CompareCell, CompareFromArchiveResult, CompareOptions as AnalyzeCompareOptions, CompareTable, CompareTotals, ComputeQualityOptions, CostBreakdown, CoverageField, FidelitySummary, FieldCoverage, FileAggregation, HotspotsOptions as AnalyzeHotspotsOptions, HotspotsResult as AnalyzeHotspotsResult, MarkdownSection, McpServerAggregation, ModelCost, OneShotMetrics, OutcomeLabel, OverheadAttribution, OverheadFile, OverheadFileAttribution, OverheadFileKind, - ParsedOverheadFile, PricingTable, ProviderAggregateRow, QualityResult, ReasoningMode, - ReplacementSavingsSummary, RowCoverage, SessionClaudeMdCost, SessionOutcome, SessionTotals, - SubagentAggregation, SubagentTreeNode, SubagentTypeStats, ToolAttribution, TrimRecommendation, - TurnProvider, UsageCostAggregateRow, WasteFinding, WasteSeverity, DEFAULT_MIN_SAMPLE, + ParsedOverheadFile, PricingTable, ProviderAggregateRow, ProviderFilter, ProviderRule, + QualityResult, ReasoningMode, ReplacementSavingsSummary, RowCoverage, SessionClaudeMdCost, + SessionOutcome, SessionTotals, SubagentAggregation, SubagentTreeNode, SubagentTypeStats, + ToolAttribution, TrimRecommendation, TurnProvider, UsageCostAggregateRow, WasteFinding, + WasteSeverity, DEFAULT_MIN_SAMPLE, }; // Span tree primitives (issue #430). Re-exported at the SDK root so @@ -134,11 +134,10 @@ pub use crate::analyze::{ pub use crate::ingest::{ cleanup_stale_pending_stamps, default_session_roots, ingest_all, ingest_claude_session, ingest_claude_transcript_path, ingest_codex_sessions, ingest_opencode_sessions, - start_watch_loop, write_pending_stamp, ErrorSink, IngestFn, - IngestOptions as RawIngestOptions, IngestReport, IngestRoots, PendingStamp, - PendingStampHarness, PendingStampWriteResult, ReportSink, StartWatchLoopOptions, - WatchController, WriteOptions as PendingStampWriteOptions, DEFAULT_FS_DEBOUNCE, - DEFAULT_SLOW_FALLBACK, + start_watch_loop, write_pending_stamp, ErrorSink, IngestFn, IngestOptions as RawIngestOptions, + IngestReport, IngestRoots, PendingStamp, PendingStampHarness, PendingStampWriteResult, + ReportSink, StartWatchLoopOptions, WatchController, WriteOptions as PendingStampWriteOptions, + DEFAULT_FS_DEBOUNCE, DEFAULT_SLOW_FALLBACK, }; // --- LedgerOpenOptions ----------------------------------------------------- diff --git a/crates/relayburn-sdk/src/query_verbs.rs b/crates/relayburn-sdk/src/query_verbs.rs index 663a8835..5c7b1c58 100644 --- a/crates/relayburn-sdk/src/query_verbs.rs +++ b/crates/relayburn-sdk/src/query_verbs.rs @@ -674,7 +674,7 @@ fn total_tokens_for_turn(t: &TurnRecord) -> u64 { // richer summary report // --------------------------------------------------------------------------- -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SummaryReportOptions { pub session: Option, @@ -697,24 +697,6 @@ pub struct SummaryReportOptions { pub ledger_home: Option, } -impl Default for SummaryReportOptions { - fn default() -> Self { - Self { - session: None, - project: None, - since: None, - workflow: None, - tags: None, - group_by_tag: None, - agent: None, - providers: None, - mode: SummaryReportMode::default(), - include_quality: false, - ledger_home: None, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase", tag = "kind")] pub enum SummaryReportMode { @@ -768,6 +750,7 @@ impl SummaryGroupBy { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] +#[allow(clippy::large_enum_variant)] pub enum SummaryReport { Grouped(SummaryGroupedReport), ByTool(SummaryByToolReport), @@ -802,7 +785,10 @@ pub struct SummaryGroupedReport { /// `SubagentCounts::default()`. Presenters render the /// `subagents: X paired, Y orphan` line only when /// `!subagents.is_empty()`. - #[serde(default, skip_serializing_if = "crate::reader::SubagentCounts::is_empty")] + #[serde( + default, + skip_serializing_if = "crate::reader::SubagentCounts::is_empty" + )] pub subagents: crate::reader::SubagentCounts, #[serde(default, skip_serializing_if = "Option::is_none")] pub quality: Option, @@ -1434,11 +1420,7 @@ fn summary_subagent_session_filter( || opts.since.is_some() || opts.workflow.is_some() || opts.agent.is_some() - || opts - .tags - .as_ref() - .map(|t| !t.is_empty()) - .unwrap_or(false) + || opts.tags.as_ref().map(|t| !t.is_empty()).unwrap_or(false) || opts .providers .as_ref() @@ -2412,10 +2394,7 @@ impl LedgerHandle { /// `TurnRecord` already, but the inference key is the durable /// per-API-call identity even when the harness changes how it /// chunks rows). - pub fn inferences( - &self, - opts: InferencesOptions, - ) -> Result> { + pub fn inferences(&self, opts: InferencesOptions) -> Result> { let q = build_query( opts.session.as_deref(), opts.project.as_deref(), @@ -4133,9 +4112,9 @@ pub struct FingerprintOptions { impl FingerprintOptions { fn scope(&self) -> Result { match (self.session.as_deref(), self.project.as_ref()) { - (Some(_), Some(_)) => anyhow::bail!( - "fingerprint: pass at most one of `session` or `project`" - ), + (Some(_), Some(_)) => { + anyhow::bail!("fingerprint: pass at most one of `session` or `project`") + } (Some(s), None) => Ok(FingerprintScope::Session(s.to_string())), (None, Some(p)) => Ok(FingerprintScope::Project(p.clone())), (None, None) => Ok(FingerprintScope::AllSessions), @@ -4206,9 +4185,7 @@ impl LedgerHandle { .into_iter() .find(|t| t.turn_id == turn_id) .ok_or_else(|| { - anyhow::anyhow!( - "turn not found: session_id={session_id} turn_id={turn_id}" - ) + anyhow::anyhow!("turn not found: session_id={session_id} turn_id={turn_id}") }) } @@ -4262,12 +4239,13 @@ impl LedgerHandle { // Group sidecars by message_id for fast per-turn slicing. let mut infs_by_msg: HashMap> = HashMap::new(); for inf in inferences { - infs_by_msg.entry(inf.turn_id.clone()).or_default().push(inf); + infs_by_msg + .entry(inf.turn_id.clone()) + .or_default() + .push(inf); } - let mut events_by_msg: HashMap< - String, - Vec, - > = HashMap::new(); + let mut events_by_msg: HashMap> = + HashMap::new(); for ev in tool_result_events { if let Some(m) = ev.message_id.clone() { events_by_msg.entry(m).or_default().push(ev); @@ -4298,28 +4276,30 @@ impl LedgerHandle { // - **Orphan**: assign to the latest turn whose `ts <= // subagent_start_ms`; if no turn precedes it (or the sidecar // has no parseable timestamp), assign to the first turn. - let subagent_buckets = - bucket_subagents_per_turn(&turns, &subagents); + let subagent_buckets = bucket_subagents_per_turn(&turns, &subagents); let mut out = Vec::with_capacity(turns.len()); for (turn_idx, turn) in turns.iter().enumerate() { - let infs_for_turn = infs_by_msg.get(&turn.message_id).cloned().unwrap_or_default(); - let events_for_turn = - events_by_msg.get(&turn.message_id).cloned().unwrap_or_default(); + let infs_for_turn = infs_by_msg + .get(&turn.message_id) + .cloned() + .unwrap_or_default(); + let events_for_turn = events_by_msg + .get(&turn.message_id) + .cloned() + .unwrap_or_default(); let subagents_for_turn: Vec = subagent_buckets .get(&turn_idx) .map(|idxs| idxs.iter().map(|i| subagents[*i].clone()).collect()) .unwrap_or_default(); let tree = match source { crate::reader::SourceKind::ClaudeCode => { - crate::reader::build_claude_span_tree( - crate::reader::ClaudeSpanTreeInputs { - turn, - tool_result_events: &events_for_turn, - inferences: &infs_for_turn, - subagents: &subagents_for_turn, - }, - ) + crate::reader::build_claude_span_tree(crate::reader::ClaudeSpanTreeInputs { + turn, + tool_result_events: &events_for_turn, + inferences: &infs_for_turn, + subagents: &subagents_for_turn, + }) } _ => crate::reader::build_codex_span_tree(crate::reader::CodexSpanTreeInputs { turn, @@ -4452,9 +4432,7 @@ fn parse_iso_ms_compat(s: &str) -> Option { /// We resolve `BURN_CLAUDE_PROJECTS_DIR` first to mirror what the /// summary path does (so the test suite can pin a sandbox); otherwise /// fall back to `$HOME/.claude/projects`. -fn discover_and_pair_subagents( - session_id: &str, -) -> Result> { +fn discover_and_pair_subagents(session_id: &str) -> Result> { let root = if let Some(p) = std::env::var_os("BURN_CLAUDE_PROJECTS_DIR") { std::path::PathBuf::from(p) } else { @@ -4811,7 +4789,9 @@ mod tests { // cutoff of `...12Z`, dropping valid turns. Canonicalizing widens to // `.000Z` so the cutoff is the lower bound for that second. assert_eq!( - normalize_since(Some("2026-04-01T00:00:00Z")).unwrap().as_deref(), + normalize_since(Some("2026-04-01T00:00:00Z")) + .unwrap() + .as_deref(), Some("2026-04-01T00:00:00.000Z"), ); } @@ -4819,17 +4799,23 @@ mod tests { #[test] fn normalize_since_preserves_millisecond_precision() { assert_eq!( - normalize_since(Some("2026-05-06T00:00:00.500Z")).unwrap().as_deref(), + normalize_since(Some("2026-05-06T00:00:00.500Z")) + .unwrap() + .as_deref(), Some("2026-05-06T00:00:00.500Z"), ); // Sub-millisecond digits are truncated to 3. assert_eq!( - normalize_since(Some("2026-05-06T00:00:00.500999Z")).unwrap().as_deref(), + normalize_since(Some("2026-05-06T00:00:00.500999Z")) + .unwrap() + .as_deref(), Some("2026-05-06T00:00:00.500Z"), ); // Shorter fraction is right-padded. assert_eq!( - normalize_since(Some("2026-05-06T00:00:00.5Z")).unwrap().as_deref(), + normalize_since(Some("2026-05-06T00:00:00.5Z")) + .unwrap() + .as_deref(), Some("2026-05-06T00:00:00.500Z"), ); } @@ -4839,7 +4825,9 @@ mod tests { // `-07:00` is 7h behind UTC → same wall-clock corresponds to a UTC // instant 7h later. 2026-05-06T00:00:00-07:00 == 2026-05-06T07:00:00Z. assert_eq!( - normalize_since(Some("2026-05-06T00:00:00-07:00")).unwrap().as_deref(), + normalize_since(Some("2026-05-06T00:00:00-07:00")) + .unwrap() + .as_deref(), Some("2026-05-06T07:00:00.000Z"), ); } @@ -4848,7 +4836,9 @@ mod tests { fn normalize_since_converts_positive_offset_to_utc() { // 2026-05-06T00:00:00+09:00 == 2026-05-05T15:00:00Z. assert_eq!( - normalize_since(Some("2026-05-06T00:00:00+09:00")).unwrap().as_deref(), + normalize_since(Some("2026-05-06T00:00:00+09:00")) + .unwrap() + .as_deref(), Some("2026-05-05T15:00:00.000Z"), ); } @@ -4856,7 +4846,9 @@ mod tests { #[test] fn normalize_since_accepts_lowercase_z_and_t() { assert_eq!( - normalize_since(Some("2026-05-06t00:00:00.500z")).unwrap().as_deref(), + normalize_since(Some("2026-05-06t00:00:00.500z")) + .unwrap() + .as_deref(), Some("2026-05-06T00:00:00.500Z"), ); } @@ -4959,10 +4951,7 @@ mod tests { let opts = LedgerOpenOptions::with_home(dir.path()); let mut handle = Ledger::open(opts).expect("open ledger"); - let make_turn = |idx: u64, - msg: &str, - stop_reason: Option| - -> TurnRecord { + let make_turn = |idx: u64, msg: &str, stop_reason: Option| -> TurnRecord { TurnRecord { v: 1, source: SourceKind::ClaudeCode, @@ -6544,7 +6533,10 @@ mod tests { assert_eq!(parts[0], "2", "fixture appends 2 turns"); assert!(!parts[1].is_empty(), "max_ts must be non-empty"); let total_bytes: u64 = parts[2].parse().expect("total_bytes is numeric"); - assert!(total_bytes > 0, "total_bytes must be > 0 for non-empty fixture"); + assert!( + total_bytes > 0, + "total_bytes must be > 0 for non-empty fixture" + ); } #[test] @@ -6719,8 +6711,7 @@ mod tests { assert_eq!(trees[0].root.status, SpanStatus::Ok); // Each root has UserPrompt + at least one Inference child. - let kinds: Vec = - trees[0].root.children.iter().map(|c| c.kind).collect(); + let kinds: Vec = trees[0].root.children.iter().map(|c| c.kind).collect(); assert!(kinds.contains(&SpanKind::UserPrompt)); assert!(kinds.contains(&SpanKind::Inference)); @@ -6755,11 +6746,10 @@ mod tests { #[test] fn turn_span_tree_missing_turn_errors() { let (_dir, handle) = fixture_handle(); - let err = handle.turn_span_tree("sess-a", "does-not-exist").unwrap_err(); - assert!( - err.to_string().contains("turn not found"), - "got: {err:?}" - ); + let err = handle + .turn_span_tree("sess-a", "does-not-exist") + .unwrap_err(); + assert!(err.to_string().contains("turn not found"), "got: {err:?}"); } /// Unknown session id → no trees, no error. @@ -6847,18 +6837,10 @@ mod tests { let orphan_mid = bucket_subagent("orphan-mid", None, Some("2026-04-23T00:02:00.000Z")); // Orphan timestamped before any turn — attaches to turn 0 // (first-turn fallback). - let orphan_early = bucket_subagent( - "orphan-early", - None, - Some("2026-04-22T23:00:00.000Z"), - ); + let orphan_early = bucket_subagent("orphan-early", None, Some("2026-04-22T23:00:00.000Z")); // Orphan timestamped after both turns — attaches to turn 1 // (latest preceding). - let orphan_late = bucket_subagent( - "orphan-late", - None, - Some("2026-04-23T00:10:00.000Z"), - ); + let orphan_late = bucket_subagent("orphan-late", None, Some("2026-04-23T00:10:00.000Z")); let subagents = vec![paired, orphan_mid, orphan_early, orphan_late]; let buckets = bucket_subagents_per_turn(&turns, &subagents); @@ -6892,7 +6874,10 @@ mod tests { "orphan-early -> turn0 (first-turn fallback)" ); assert!(turn1_agents.contains(&"paired-1"), "paired-1 -> turn1"); - assert!(turn1_agents.contains(&"orphan-late"), "orphan-late -> turn1"); + assert!( + turn1_agents.contains(&"orphan-late"), + "orphan-late -> turn1" + ); // No turn carries the same agent twice. assert_eq!(turn0_agents.len(), 2); assert_eq!(turn1_agents.len(), 2); diff --git a/crates/relayburn-sdk/src/reader.rs b/crates/relayburn-sdk/src/reader.rs index 8a7b208a..8f4af424 100644 --- a/crates/relayburn-sdk/src/reader.rs +++ b/crates/relayburn-sdk/src/reader.rs @@ -18,12 +18,12 @@ pub mod claude; pub mod codex; pub mod opencode; +pub use codex::span_tree::{build_codex_span_tree, CodexSpanTreeInputs}; pub use codex::{ parse_codex_session_incremental, read_codex_session_id_hint, CodexLastCompletedTurn, CodexResumeState, CodexTurnContext, CumulativeUsage, ParseCodexIncrementalOptions, ParseCodexIncrementalResult, PersistedUserTurnSlot, }; -pub use codex::span_tree::{build_codex_span_tree, CodexSpanTreeInputs}; pub use opencode::{ parse_opencode_session_incremental, ParseOpencodeIncrementalOptions, ParseOpencodeIncrementalResult, @@ -33,27 +33,26 @@ pub use classifier::{ count_retries, normalize_tool_name, parse_bash_command, BashParse, ClassificationInput, ClassificationResult, }; -pub use fidelity::classify_fidelity; -pub use git::{resolve_project, ProjectResolver, ResolvedProject}; -pub use types::{ - ActivityCategory, CompactionEvent, ContentKind, ContentRecord, ContentRole, ContentStoreMode, - ContentToolResult, ContentToolUse, Coverage, Fidelity, FidelityClass, Harness, - RelationshipSourceKind, RelationshipType, SessionRelationshipRecord, SourceKind, StopReason, - Subagent, ToolCall, ToolResultEventRecord, ToolResultEventSource, ToolResultStatus, - TurnRecord, Usage, UsageAttribution, UsageGranularity, UserTurnBlock, UserTurnBlockKind, - UserTurnRecord, +pub use claude::span_tree::{build_claude_span_tree, ClaudeSpanTreeInputs}; +pub use claude::subagents::{ + count_subagents_under, discover_subagents, pair_to_main, SubagentCounts, SubagentTranscript, }; pub use claude::{ - parse_claude_session, parse_claude_session_incremental, - reconcile_claude_session_relationships, ParseIncrementalOptions as ClaudeParseIncrementalOptions, + parse_claude_session, parse_claude_session_incremental, reconcile_claude_session_relationships, + ParseIncrementalOptions as ClaudeParseIncrementalOptions, ParseIncrementalResult as ClaudeParseIncrementalResult, ParseOptions as ClaudeParseOptions, ParseResult as ClaudeParseResult, ReconcileClaudeRelationshipsInput, }; -pub use claude::subagents::{ - count_subagents_under, discover_subagents, pair_to_main, SubagentCounts, SubagentTranscript, -}; -pub use claude::span_tree::{build_claude_span_tree, ClaudeSpanTreeInputs}; +pub use fidelity::classify_fidelity; +pub use git::{resolve_project, ProjectResolver, ResolvedProject}; pub use inference::{ build_inferences, Inference, InferenceKeySource, InferenceKind, RequestIdLookup, ToolUseRef, TurnKey, }; +pub use types::{ + ActivityCategory, CompactionEvent, ContentKind, ContentRecord, ContentRole, ContentStoreMode, + ContentToolResult, ContentToolUse, Coverage, Fidelity, FidelityClass, Harness, + RelationshipSourceKind, RelationshipType, SessionRelationshipRecord, SourceKind, StopReason, + Subagent, ToolCall, ToolResultEventRecord, ToolResultEventSource, ToolResultStatus, TurnRecord, + Usage, UsageAttribution, UsageGranularity, UserTurnBlock, UserTurnBlockKind, UserTurnRecord, +}; diff --git a/crates/relayburn-sdk/src/reader/claude.rs b/crates/relayburn-sdk/src/reader/claude.rs index 1a86098f..48ab8b49 100644 --- a/crates/relayburn-sdk/src/reader/claude.rs +++ b/crates/relayburn-sdk/src/reader/claude.rs @@ -316,10 +316,7 @@ impl ChainNode for LineNode { /// Reads `LineNode`s through the `ChainNode` trait so the walker stays /// in sync with the standalone `group_by_parent_chain` helper (same /// trait, same termination rules). -fn nearest_user_prompt_root( - start_uuid: &str, - nodes: &HashMap, -) -> Option { +fn nearest_user_prompt_root(start_uuid: &str, nodes: &HashMap) -> Option { let mut visited: HashSet<&str> = HashSet::new(); let mut current: &LineNode = nodes.get(start_uuid)?; visited.insert(current.uuid()); @@ -402,8 +399,7 @@ fn ingest_assistant_record( // inside `message`. Capture from the first row that carries one for // this `message_id`; later rows belonging to the same API call // re-emit the same `requestId` so first-wins is the right merge. - let request_id = - string_field(obj, &["requestId", "request_id"], true); + let request_id = string_field(obj, &["requestId", "request_id"], true); let usage_with_cov = to_usage(msg.get("usage")); // Claude writes one row per content block but only ONE of those rows // carries the `usage` block. The previous merge updated @@ -531,11 +527,7 @@ fn register_assistant_node(line: &Value, nodes: &mut HashMap) nodes.insert(node.uuid.clone(), node); } -fn register_user_node( - line: &Value, - nodes: &mut HashMap, - is_user_prompt: bool, -) { +fn register_user_node(line: &Value, nodes: &mut HashMap, is_user_prompt: bool) { let mut node = match make_line_node(line, LineKind::User) { Some(n) => n, None => return, @@ -830,11 +822,8 @@ fn resolve_invocation( } let mut current_uuid = start_uuid.to_string(); let mut visited: HashSet = HashSet::new(); - loop { - let node = match nodes.get(¤t_uuid) { - Some(n) => n.clone(), - None => break, - }; + while let Some(n) = nodes.get(¤t_uuid) { + let node = n.clone(); if !visited.insert(node.uuid.clone()) { break; } @@ -846,34 +835,35 @@ fn resolve_invocation( Some(p) => p.clone(), None => break, }; - let parent_agent = parent.agent_tool_use.clone(); - if node.kind == LineKind::User - && parent.kind == LineKind::Assistant - && parent_agent.is_some() - && !node - .tool_result_ids - .as_ref() - .is_some_and(|ids| ids.contains(&parent_agent.as_ref().unwrap().id)) - { - let pat = parent_agent.unwrap(); - let mut info = InvocationInfo { - root_uuid: node.uuid.clone(), - parent_tool_use_id: if pat.id.is_empty() { - None - } else { - Some(pat.id.clone()) - }, - subagent_type: pat.subagent_type.clone(), - description: pat.description.clone(), - parent_agent_id: None, - }; - if parent.is_sidechain { - if let Some(pi) = resolve_invocation(&parent.uuid, nodes, cache, depth + 1) { - info.parent_agent_id = Some(pi.root_uuid); + match parent.agent_tool_use.clone() { + Some(pat) + if node.kind == LineKind::User + && parent.kind == LineKind::Assistant + && !node + .tool_result_ids + .as_ref() + .is_some_and(|ids| ids.contains(&pat.id)) => + { + let mut info = InvocationInfo { + root_uuid: node.uuid.clone(), + parent_tool_use_id: if pat.id.is_empty() { + None + } else { + Some(pat.id.clone()) + }, + subagent_type: pat.subagent_type.clone(), + description: pat.description.clone(), + parent_agent_id: None, + }; + if parent.is_sidechain { + if let Some(pi) = resolve_invocation(&parent.uuid, nodes, cache, depth + 1) { + info.parent_agent_id = Some(pi.root_uuid); + } } + cache.insert(start_uuid.to_string(), Some(info.clone())); + return Some(info); } - cache.insert(start_uuid.to_string(), Some(info.clone())); - return Some(info); + _ => {} } current_uuid = parent_uuid; } @@ -1259,7 +1249,7 @@ fn measure_tool_result(content: &Value) -> Measured { return Measured { length: Some(s.chars().count() as u64), hash: Some(content_hash(s)), - byte_length: Some(s.as_bytes().len() as u64), + byte_length: Some(s.len() as u64), truncated: Some(detect_truncation_marker(s)), }; } @@ -1270,7 +1260,7 @@ fn measure_tool_result(content: &Value) -> Measured { Ok(s) => Measured { length: Some(s.chars().count() as u64), hash: Some(content_hash(&s)), - byte_length: Some(s.as_bytes().len() as u64), + byte_length: Some(s.len() as u64), truncated: Some(detect_truncation_marker(&s)), }, Err(_) => Measured::default(), @@ -1746,18 +1736,11 @@ fn append_unique(values: Option>, value: String) -> Vec { /// for cross-line dedup; cheap because the original `relationship_key` did one /// `format!`-driven allocation per call but had to be re-run for every /// candidate during `has_relationship`. -type RelationshipKey = ( - &'static str, - String, - &'static str, - String, - String, - String, -); - -fn relationship_key_borrowed<'a>( - row: &'a SessionRelationshipRecord, -) -> (&'static str, &'a str, &'static str, &'a str, &'a str, &'a str) { +type RelationshipKey = (&'static str, String, &'static str, String, String, String); + +fn relationship_key_borrowed( + row: &SessionRelationshipRecord, +) -> (&'static str, &str, &'static str, &str, &str, &str) { ( row.source.wire_str(), row.session_id.as_str(), @@ -1770,7 +1753,14 @@ fn relationship_key_borrowed<'a>( fn relationship_key(row: &SessionRelationshipRecord) -> RelationshipKey { let b = relationship_key_borrowed(row); - (b.0, b.1.to_string(), b.2, b.3.to_string(), b.4.to_string(), b.5.to_string()) + ( + b.0, + b.1.to_string(), + b.2, + b.3.to_string(), + b.4.to_string(), + b.5.to_string(), + ) } fn has_relationship(rows: &[SessionRelationshipRecord], row: &SessionRelationshipRecord) -> bool { @@ -2205,8 +2195,7 @@ fn prescan_nodes( // the task-notification gate and #433 for why we tag // the LineNode for the parent-chain walker. let is_user_prompt = !is_task_notification(&obj) - && extract_plain_user_text_from_obj(&obj) - .is_some_and(|s| !s.is_empty()); + && extract_plain_user_text_from_obj(&obj).is_some_and(|s| !s.is_empty()); register_user_node(&parsed, nodes_by_uuid, is_user_prompt); record_evidence_from_line(evidence, &parsed); record_explicit_relationship_evidence(evidence, &obj); @@ -2219,16 +2208,15 @@ fn prescan_nodes( next_event_index, ); } - "system" => { + "system" if build_claude_system_tool_result_event( &obj, tool_result_counters, next_event_index, ) - .is_some() - { - next_event_index += 1; - } + .is_some() => + { + next_event_index += 1; } _ => {} } @@ -3119,7 +3107,10 @@ mod tests { assert_eq!(full.user_turns, vec![user_turn]); assert_eq!(full.evidence.file_session_id, evidence.file_session_id); assert_eq!(full.evidence.first_ts, evidence.first_ts); - assert_eq!(full.evidence.in_log_session_ids, evidence.in_log_session_ids); + assert_eq!( + full.evidence.in_log_session_ids, + evidence.in_log_session_ids + ); assert_eq!(full.evidence.source_version, evidence.source_version); assert_eq!(full.evidence.first_parent_uuid, evidence.first_parent_uuid); assert_eq!(full.evidence.seen_uuids, evidence.seen_uuids); diff --git a/crates/relayburn-sdk/src/reader/claude/parent_chain.rs b/crates/relayburn-sdk/src/reader/claude/parent_chain.rs index fa33c186..a02c89f7 100644 --- a/crates/relayburn-sdk/src/reader/claude/parent_chain.rs +++ b/crates/relayburn-sdk/src/reader/claude/parent_chain.rs @@ -341,10 +341,7 @@ mod tests { /// issue's "prefer over-grouping to silently dropping rows" guidance. #[test] fn orphan_chain_uses_deepest_reachable_uuid() { - let rows = vec![ - asst("a1", "missing_parent"), - asst("a2", "a1"), - ]; + let rows = vec![asst("a1", "missing_parent"), asst("a2", "a1")]; let g = group_by_parent_chain(&rows); // a2 walks up to a1, then a1's parent ("missing_parent") is not in // the map → bucket key is "a1". a1 also walks once and hits the diff --git a/crates/relayburn-sdk/src/reader/claude/span_tree.rs b/crates/relayburn-sdk/src/reader/claude/span_tree.rs index 5dfc0574..a6e0ffb2 100644 --- a/crates/relayburn-sdk/src/reader/claude/span_tree.rs +++ b/crates/relayburn-sdk/src/reader/claude/span_tree.rs @@ -140,10 +140,9 @@ pub fn build_claude_span_tree(inputs: ClaudeSpanTreeInputs<'_>) -> TurnSpanTree let mut unpaired_subagents: Vec<&SubagentTranscript> = Vec::new(); for sa in inputs.subagents { match sa.paired_tool_use_id.as_deref() { - Some(id) if !id.is_empty() => paired_subagents - .entry(id.to_string()) - .or_default() - .push(sa), + Some(id) if !id.is_empty() => { + paired_subagents.entry(id.to_string()).or_default().push(sa) + } _ => unpaired_subagents.push(sa), } } @@ -153,23 +152,15 @@ pub fn build_claude_span_tree(inputs: ClaudeSpanTreeInputs<'_>) -> TurnSpanTree // is_error flag, etc.) lives on the turn's `tool_calls`. Index the // turn's calls by id so we can hydrate the `ToolUse` span with the // richer per-call attributes without re-parsing. - let toolcall_by_id: HashMap<&str, &ToolCall> = turn - .tool_calls - .iter() - .map(|c| (c.id.as_str(), c)) - .collect(); + let toolcall_by_id: HashMap<&str, &ToolCall> = + turn.tool_calls.iter().map(|c| (c.id.as_str(), c)).collect(); let inference_pairs = effective_inferences(turn, inputs.inferences); let mut last_inference_end: i64 = root.end_ms; for inf in inference_pairs.iter() { - let inference_node = build_inference_node( - inf, - turn, - &toolcall_by_id, - &tr_by_id, - &mut paired_subagents, - ); + let inference_node = + build_inference_node(inf, turn, &toolcall_by_id, &tr_by_id, &mut paired_subagents); last_inference_end = last_inference_end.max(inference_node.end_ms); root.children.push(inference_node); } @@ -187,10 +178,7 @@ pub fn build_claude_span_tree(inputs: ClaudeSpanTreeInputs<'_>) -> TurnSpanTree paired_subagents.into_values().flatten().collect(); dangling_paired.sort_by(|a, b| a.source_path.cmp(&b.source_path)); - for sa in unpaired_subagents - .into_iter() - .chain(dangling_paired.into_iter()) - { + for sa in unpaired_subagents.into_iter().chain(dangling_paired) { root.children.push(build_subagent_node(sa, true)); } @@ -256,7 +244,9 @@ fn attach_token_attrs(node: &mut SpanNode, u: &UsageView) { /// Group `ToolResultEventRecord`s by `tool_use_id`. Each id may have /// multiple events (progress + final status); the slice is preserved in /// insertion order so a presenter can render the timeline. -fn index_tool_results(events: &[ToolResultEventRecord]) -> HashMap> { +fn index_tool_results( + events: &[ToolResultEventRecord], +) -> HashMap> { let mut out: HashMap> = HashMap::new(); for ev in events { out.entry(ev.tool_use_id.clone()).or_default().push(ev); @@ -277,7 +267,9 @@ fn effective_inferences<'a>(turn: &TurnRecord, supplied: &'a [Inference]) -> Vec if !supplied.is_empty() { return supplied.iter().map(InferenceRef::Borrowed).collect(); } - vec![InferenceRef::Synthetic(synthesize_inference(turn))] + vec![InferenceRef::Synthetic(Box::new(synthesize_inference( + turn, + )))] } /// Either a caller-supplied Inference (when #434 was wired) or a @@ -286,14 +278,14 @@ fn effective_inferences<'a>(turn: &TurnRecord, supplied: &'a [Inference]) -> Vec /// `Cow` (which would require `Inference: ToOwned`). enum InferenceRef<'a> { Borrowed(&'a Inference), - Synthetic(Inference), + Synthetic(Box), } impl<'a> InferenceRef<'a> { fn as_ref(&self) -> &Inference { match self { InferenceRef::Borrowed(b) => b, - InferenceRef::Synthetic(s) => s, + InferenceRef::Synthetic(s) => s.as_ref(), } } } @@ -439,7 +431,10 @@ fn build_tool_result_node(events: &[&ToolResultEventRecord]) -> Option .unwrap_or(*events.last().unwrap()); let mut node = SpanNode::new(SpanKind::ToolResult, "tool-result"); - node.set_attr("tool_use_id", AttrValue::str(final_event.tool_use_id.clone())); + node.set_attr( + "tool_use_id", + AttrValue::str(final_event.tool_use_id.clone()), + ); if let Some(ts) = final_event.ts.as_deref() { let ms = parse_iso_ms(ts).unwrap_or(0); node.start_ms = ms; @@ -461,7 +456,10 @@ fn build_tool_result_node(events: &[&ToolResultEventRecord]) -> Option // Surface status as an attribute too — useful for downstream // presenters that want to display "cancelled" / "errored" without // having to interpret `SpanStatus`. - node.set_attr("status", AttrValue::str(format!("{:?}", final_event.status).to_ascii_lowercase())); + node.set_attr( + "status", + AttrValue::str(format!("{:?}", final_event.status).to_ascii_lowercase()), + ); Some(node) } @@ -514,12 +512,9 @@ fn apply_stop_reason_status(root: &mut SpanNode, reason: Option) { #[cfg(test)] mod tests { use super::*; - use crate::reader::inference::{ - Inference, InferenceKeySource, InferenceKind, ToolUseRef, - }; + use crate::reader::inference::{Inference, InferenceKeySource, InferenceKind, ToolUseRef}; use crate::reader::types::{ - SourceKind, ToolCall, ToolResultEventRecord, ToolResultEventSource, ToolResultStatus, - Usage, + SourceKind, ToolCall, ToolResultEventRecord, ToolResultEventSource, ToolResultStatus, Usage, }; fn make_turn(usage: Usage, calls: Vec) -> TurnRecord { @@ -650,10 +645,7 @@ mod tests { // Root + UserPrompt + Inference -> ToolUse. let child_kinds: Vec = tree.root.children.iter().map(|c| c.kind).collect(); - assert_eq!( - child_kinds, - vec![SpanKind::UserPrompt, SpanKind::Inference] - ); + assert_eq!(child_kinds, vec![SpanKind::UserPrompt, SpanKind::Inference]); let inf_node = &tree.root.children[1]; assert_eq!(inf_node.children.len(), 1); assert_eq!(inf_node.children[0].kind, SpanKind::ToolUse); @@ -959,11 +951,14 @@ mod tests { .unwrap(); let tool_use = &inf_node.children[0]; assert_eq!(tool_use.name, "Task"); - let nested: Vec<&SpanNode> = - tool_use.children.iter().filter(|c| c.kind == SpanKind::Subagent).collect(); + let nested: Vec<&SpanNode> = tool_use + .children + .iter() + .filter(|c| c.kind == SpanKind::Subagent) + .collect(); assert_eq!(nested.len(), 1); // Paired subagent does NOT carry the unattached flag. - assert!(nested[0].attributes.get("unattached").is_none()); + assert!(!nested[0].attributes.contains_key("unattached")); } /// Empty-inferences input: builder synthesizes one inference from @@ -984,8 +979,12 @@ mod tests { inferences: &[], subagents: &[], }); - let inf_children: Vec<&SpanNode> = - tree.root.children.iter().filter(|c| c.kind == SpanKind::Inference).collect(); + let inf_children: Vec<&SpanNode> = tree + .root + .children + .iter() + .filter(|c| c.kind == SpanKind::Inference) + .collect(); assert_eq!(inf_children.len(), 1); // Synthetic inference carries the message_id as request_id. match inf_children[0].attributes.get("request_id") { diff --git a/crates/relayburn-sdk/src/reader/claude/subagents.rs b/crates/relayburn-sdk/src/reader/claude/subagents.rs index 0197048f..f609ea08 100644 --- a/crates/relayburn-sdk/src/reader/claude/subagents.rs +++ b/crates/relayburn-sdk/src/reader/claude/subagents.rs @@ -277,7 +277,9 @@ fn extract_agent_id_to_tool_use_id(main: &[Value]) -> std::collections::HashMap< if bo.get("type").and_then(Value::as_str)? != "tool_result" { return None; } - bo.get("tool_use_id").and_then(Value::as_str).map(str::to_string) + bo.get("tool_use_id") + .and_then(Value::as_str) + .map(str::to_string) }) }); if let Some(tu) = tool_use_id { @@ -833,15 +835,9 @@ mod tests { let by_id: std::collections::HashMap<&str, &SubagentTranscript> = paired.iter().map(|t| (t.agent_id.as_str(), t)).collect(); - assert_eq!( - by_id["aaa"].paired_tool_use_id.as_deref(), - Some("toolu_a") - ); + assert_eq!(by_id["aaa"].paired_tool_use_id.as_deref(), Some("toolu_a")); assert_eq!(by_id["aaa"].agent_type.as_deref(), Some("general-purpose")); - assert_eq!( - by_id["bbb"].paired_tool_use_id.as_deref(), - Some("toolu_b") - ); + assert_eq!(by_id["bbb"].paired_tool_use_id.as_deref(), Some("toolu_b")); assert_eq!(by_id["bbb"].agent_type.as_deref(), Some("code-reviewer")); assert!( by_id["ccc"].paired_tool_use_id.is_none(), diff --git a/crates/relayburn-sdk/src/reader/codex.rs b/crates/relayburn-sdk/src/reader/codex.rs index bca0e24d..8021fd8e 100644 --- a/crates/relayburn-sdk/src/reader/codex.rs +++ b/crates/relayburn-sdk/src/reader/codex.rs @@ -410,9 +410,7 @@ fn parse_codex_buffer( } let line_end_offset = current_offset + n as u64; current_offset = line_end_offset; - let text = std::str::from_utf8(&line_buf[..n - 1]) - .unwrap_or("") - .trim(); + let text = std::str::from_utf8(&line_buf[..n - 1]).unwrap_or("").trim(); if text.is_empty() { continue; } @@ -1431,13 +1429,13 @@ fn measure_tool_output(output: &Value) -> Measured { Value::String(s) => Measured { length: Some(s.len() as u64), hash: Some(content_hash(s)), - byte_length: Some(s.as_bytes().len() as u64), + byte_length: Some(s.len() as u64), }, other => match serde_json::to_string(other) { Ok(serialized) => Measured { length: Some(serialized.len() as u64), hash: Some(content_hash(&serialized)), - byte_length: Some(serialized.as_bytes().len() as u64), + byte_length: Some(serialized.len() as u64), }, Err(_) => Measured::default(), }, diff --git a/crates/relayburn-sdk/src/reader/codex/span_tree.rs b/crates/relayburn-sdk/src/reader/codex/span_tree.rs index 5d3e9192..05410618 100644 --- a/crates/relayburn-sdk/src/reader/codex/span_tree.rs +++ b/crates/relayburn-sdk/src/reader/codex/span_tree.rs @@ -83,11 +83,8 @@ pub fn build_codex_span_tree(inputs: CodexSpanTreeInputs<'_>) -> TurnSpanTree { root.children.push(user_prompt); let tr_by_id = index_tool_results(inputs.tool_result_events); - let toolcall_by_id: HashMap<&str, &ToolCall> = turn - .tool_calls - .iter() - .map(|c| (c.id.as_str(), c)) - .collect(); + let toolcall_by_id: HashMap<&str, &ToolCall> = + turn.tool_calls.iter().map(|c| (c.id.as_str(), c)).collect(); let inferences = effective_inferences(turn, inputs.inferences); @@ -150,7 +147,9 @@ fn attach_token_attrs(node: &mut SpanNode, u: &UsageView) { node.set_attr("tokens.reasoning", AttrValue::Int(u.reasoning)); } -fn index_tool_results(events: &[ToolResultEventRecord]) -> HashMap> { +fn index_tool_results( + events: &[ToolResultEventRecord], +) -> HashMap> { let mut out: HashMap> = HashMap::new(); for ev in events { out.entry(ev.tool_use_id.clone()).or_default().push(ev); @@ -272,7 +271,10 @@ fn build_tool_result_node(events: &[&ToolResultEventRecord]) -> Option .copied() .unwrap_or(*events.last().unwrap()); let mut node = SpanNode::new(SpanKind::ToolResult, "tool-result"); - node.set_attr("tool_use_id", AttrValue::str(final_event.tool_use_id.clone())); + node.set_attr( + "tool_use_id", + AttrValue::str(final_event.tool_use_id.clone()), + ); if let Some(ts) = final_event.ts.as_deref() { let ms = parse_iso_ms(ts).unwrap_or(0); node.start_ms = ms; @@ -446,7 +448,10 @@ mod tests { let kinds: Vec = tree.root.children.iter().map(|c| c.kind).collect(); assert_eq!(kinds, vec![SpanKind::UserPrompt, SpanKind::Inference]); let inf = &tree.root.children[1]; - assert!(inf.children.is_empty(), "no tool_calls => no ToolUse children"); + assert!( + inf.children.is_empty(), + "no tool_calls => no ToolUse children" + ); } /// Regression: a `ToolResult` whose timestamp lands after the diff --git a/crates/relayburn-sdk/src/reader/hash.rs b/crates/relayburn-sdk/src/reader/hash.rs index ff4ed269..623bb95b 100644 --- a/crates/relayburn-sdk/src/reader/hash.rs +++ b/crates/relayburn-sdk/src/reader/hash.rs @@ -221,11 +221,7 @@ impl<'a> Serializer for StableSerializer<'a> { current_key: None, }) } - fn serialize_struct( - self, - _: &'static str, - len: usize, - ) -> Result, StableError> { + fn serialize_struct(self, _: &'static str, len: usize) -> Result, StableError> { Ok(StableMap { out: self.out, entries: Vec::with_capacity(len), @@ -605,10 +601,7 @@ mod tests { zebra: u32, apple: u32, } - let s = stable_stringify(&Args { - zebra: 1, - apple: 2, - }); + let s = stable_stringify(&Args { zebra: 1, apple: 2 }); assert_eq!(s, r#"{"apple":2,"zebra":1}"#); } diff --git a/crates/relayburn-sdk/src/reader/inference.rs b/crates/relayburn-sdk/src/reader/inference.rs index 9bf2cf0d..1dc74ef4 100644 --- a/crates/relayburn-sdk/src/reader/inference.rs +++ b/crates/relayburn-sdk/src/reader/inference.rs @@ -191,7 +191,11 @@ impl TurnKey { } fn sort_tuple(&self) -> (&'static str, &str, &str) { - (self.source.wire_str(), self.session_id.as_str(), self.message_id.as_str()) + ( + self.source.wire_str(), + self.session_id.as_str(), + self.message_id.as_str(), + ) } } @@ -352,11 +356,7 @@ struct KeyPair { source: InferenceKeySource, } -fn derive_inference_key( - turn: &TurnRecord, - idx: usize, - lookup: &RequestIdLookup, -) -> KeyPair { +fn derive_inference_key(turn: &TurnRecord, idx: usize, lookup: &RequestIdLookup) -> KeyPair { if let Some(req) = lookup.get(&TurnKey::for_turn(turn)) { if !req.is_empty() { return KeyPair { @@ -404,7 +404,9 @@ fn parse_iso_ms(s: &str) -> Option { if bytes.len() < 19 { return None; } - if !(bytes[4] == b'-' && bytes[7] == b'-' && (bytes[10] == b'T' || bytes[10] == b' ') + if !(bytes[4] == b'-' + && bytes[7] == b'-' + && (bytes[10] == b'T' || bytes[10] == b' ') && bytes[13] == b':' && bytes[16] == b':') { @@ -424,7 +426,9 @@ fn parse_iso_ms(s: &str) -> Option { while idx < bytes.len() && bytes[idx].is_ascii_digit() { idx += 1; } - let mut frac = std::str::from_utf8(&bytes[frac_start..idx]).ok()?.to_string(); + let mut frac = std::str::from_utf8(&bytes[frac_start..idx]) + .ok()? + .to_string(); if frac.len() > 3 { frac.truncate(3); } @@ -445,10 +449,8 @@ fn parse_iso_ms(s: &str) -> Option { let doy = (153 * mp + 2) / 5 + (d as u64) - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; let days_from_epoch = era * 146_097 + (doe as i64) - 719_468; - let secs = days_from_epoch * 86_400 - + (hour as i64) * 3_600 - + (minute as i64) * 60 - + (second as i64); + let secs = + days_from_epoch * 86_400 + (hour as i64) * 3_600 + (minute as i64) * 60 + (second as i64); Some(secs * 1_000 + millis) } @@ -533,7 +535,13 @@ mod tests { #[test] fn missing_request_id_falls_back_to_message_id() { - let t = turn("s1", "msg-1", "2026-04-20T00:00:01.000Z", Usage::default(), vec![]); + let t = turn( + "s1", + "msg-1", + "2026-04-20T00:00:01.000Z", + Usage::default(), + vec![], + ); let infs = build_inferences(&[t], &RequestIdLookup::new()); assert_eq!(infs.len(), 1); assert_eq!(infs[0].request_id, "msg-1"); @@ -542,7 +550,13 @@ mod tests { #[test] fn missing_message_id_falls_back_to_row_synthetic() { - let t = turn("s1", "", "2026-04-20T00:00:01.000Z", Usage::default(), vec![]); + let t = turn( + "s1", + "", + "2026-04-20T00:00:01.000Z", + Usage::default(), + vec![], + ); let infs = build_inferences(&[t], &RequestIdLookup::new()); assert_eq!(infs.len(), 1); assert_eq!(infs[0].request_id_source, InferenceKeySource::RowSynthetic); @@ -563,7 +577,13 @@ mod tests { replaced_tools: None, collapsed_calls: None, }; - let t = turn("s1", "msg-1", "2026-04-20T00:00:01.000Z", Usage::default(), vec![tc]); + let t = turn( + "s1", + "msg-1", + "2026-04-20T00:00:01.000Z", + Usage::default(), + vec![tc], + ); let infs = build_inferences(&[t], &RequestIdLookup::new()); assert_eq!(infs[0].kind, InferenceKind::ToolUse); assert_eq!(infs[0].tool_uses.len(), 1); @@ -622,8 +642,20 @@ mod tests { fn different_session_with_same_request_id_stays_separate() { // Two sessions, both ship `requestId=req-1`. The composite // storage key includes session_id so they don't collide. - let t1 = turn("s1", "msg-1", "2026-04-20T00:00:01.000Z", Usage::default(), vec![]); - let t2 = turn("s2", "msg-2", "2026-04-20T00:00:02.000Z", Usage::default(), vec![]); + let t1 = turn( + "s1", + "msg-1", + "2026-04-20T00:00:01.000Z", + Usage::default(), + vec![], + ); + let t2 = turn( + "s2", + "msg-2", + "2026-04-20T00:00:02.000Z", + Usage::default(), + vec![], + ); let mut lookup = RequestIdLookup::new(); lookup.insert(TurnKey::for_turn(&t1), "req-1".to_string()); lookup.insert(TurnKey::for_turn(&t2), "req-1".to_string()); diff --git a/crates/relayburn-sdk/src/reader/opencode.rs b/crates/relayburn-sdk/src/reader/opencode.rs index 83635fa0..4dc32b68 100644 --- a/crates/relayburn-sdk/src/reader/opencode.rs +++ b/crates/relayburn-sdk/src/reader/opencode.rs @@ -841,13 +841,13 @@ fn measure_opencode_tool_output(output: Option<&Value>) -> Measured { Some(Value::String(s)) => Measured { length: Some(s.len() as u64), hash: Some(content_hash(s)), - byte_length: Some(s.as_bytes().len() as u64), + byte_length: Some(s.len() as u64), }, Some(other) => match serde_json::to_string(other) { Ok(serialized) => Measured { length: Some(serialized.len() as u64), hash: Some(content_hash(&serialized)), - byte_length: Some(serialized.as_bytes().len() as u64), + byte_length: Some(serialized.len() as u64), }, Err(_) => Measured::default(), }, diff --git a/crates/relayburn-sdk/src/reader/opencode/tests.rs b/crates/relayburn-sdk/src/reader/opencode/tests.rs index ad5ce258..2f12eade 100644 --- a/crates/relayburn-sdk/src/reader/opencode/tests.rs +++ b/crates/relayburn-sdk/src/reader/opencode/tests.rs @@ -187,7 +187,10 @@ fn classifies_activity_via_aliased_tool_names() { let r = parse("with-tool", "ses_tool"); let t = &r.turns[0]; assert_eq!(t.has_edits, Some(true)); - assert_eq!(t.activity.unwrap(), crate::reader::types::ActivityCategory::Coding); + assert_eq!( + t.activity.unwrap(), + crate::reader::types::ActivityCategory::Coding + ); } // --------------------------------------------------------------------------- diff --git a/crates/relayburn-sdk/src/reader/types.rs b/crates/relayburn-sdk/src/reader/types.rs index c88c1311..14ceee50 100644 --- a/crates/relayburn-sdk/src/reader/types.rs +++ b/crates/relayburn-sdk/src/reader/types.rs @@ -19,7 +19,9 @@ use serde_json::Value; /// `tool-calls`, etc.). An unrecognized string decodes to /// [`StopReason::Silent`] instead of an error so a pre-3.0 ledger replays /// cleanly through the new column. -fn deserialize_optional_stop_reason<'de, D>(d: D) -> std::result::Result, D::Error> +fn deserialize_optional_stop_reason<'de, D>( + d: D, +) -> std::result::Result, D::Error> where D: Deserializer<'de>, { @@ -779,18 +781,12 @@ mod tests { #[test] fn stop_reason_from_wire_normalizes_underscored_and_legacy_variants() { // Anthropic snake_case. - assert_eq!( - StopReason::from_wire("end_turn"), - Some(StopReason::EndTurn) - ); + assert_eq!(StopReason::from_wire("end_turn"), Some(StopReason::EndTurn)); assert_eq!( StopReason::from_wire("max_tokens"), Some(StopReason::MaxTokens) ); - assert_eq!( - StopReason::from_wire("tool_use"), - Some(StopReason::ToolUse) - ); + assert_eq!(StopReason::from_wire("tool_use"), Some(StopReason::ToolUse)); // Opencode finish reason for the same outcome ships as `tool-calls`. assert_eq!( StopReason::from_wire("tool-calls"), diff --git a/crates/relayburn-sdk/src/stamp_verb.rs b/crates/relayburn-sdk/src/stamp_verb.rs index 3f6a8cc4..4558df97 100644 --- a/crates/relayburn-sdk/src/stamp_verb.rs +++ b/crates/relayburn-sdk/src/stamp_verb.rs @@ -75,9 +75,7 @@ fn now_iso(now: &std::time::SystemTime) -> String { .unwrap_or(0); let dt = time::OffsetDateTime::from_unix_timestamp(secs as i64) .unwrap_or(time::OffsetDateTime::UNIX_EPOCH); - let fmt = time::macros::format_description!( - "[year]-[month]-[day]T[hour]:[minute]:[second]Z" - ); + let fmt = time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"); dt.format(&fmt).expect("format z iso") } @@ -138,8 +136,7 @@ mod tests { ledger_home: Some(dir.path().to_path_buf()), }) .unwrap(); - let handle = - Ledger::open(LedgerOpenOptions::with_home(dir.path())).unwrap(); + let handle = Ledger::open(LedgerOpenOptions::with_home(dir.path())).unwrap(); let stamps = handle.inner.list_stamps().unwrap(); let ts = &stamps[0].ts; assert!( diff --git a/crates/relayburn-sdk/src/util/time.rs b/crates/relayburn-sdk/src/util/time.rs index 6f745d36..61b12cd8 100644 --- a/crates/relayburn-sdk/src/util/time.rs +++ b/crates/relayburn-sdk/src/util/time.rs @@ -76,7 +76,10 @@ mod tests { #[test] fn parse_with_fractional() { // 2026-01-01T00:00:00.500Z == 1767225600500 - assert_eq!(parse_iso_ms("2026-01-01T00:00:00.500Z"), Some(1_767_225_600_500)); + assert_eq!( + parse_iso_ms("2026-01-01T00:00:00.500Z"), + Some(1_767_225_600_500) + ); } #[test]