Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9320871
feat(core): isolate source failure domains behind data health
makoMakoGo Jul 13, 2026
7547da0
feat(zed): reject damaged thread records instead of failing the source
makoMakoGo Jul 13, 2026
3dc6e56
feat(fable): persist source health through cache layers
makoMakoGo Jul 13, 2026
46f1043
feat(core): retain valid records from damaged local sources
makoMakoGo Jul 13, 2026
3c180b7
feat(tui): expose degraded source health in an issues tab
makoMakoGo Jul 13, 2026
c264ff0
feat(cli): report source health without failing reports
makoMakoGo Jul 13, 2026
2117400
feat(fable): isolate malformed records across local parsers
makoMakoGo Jul 14, 2026
65500b5
fix(cache): rebuild unreadable shards without terminal warnings
makoMakoGo Jul 14, 2026
a041cd3
fix(health): bound issue reporting and rescan invalid caches
makoMakoGo Jul 14, 2026
b0fb711
fix(health): clarify source status reporting and issues UI
makoMakoGo Jul 14, 2026
054fb00
fix(tui): remove needless borrow in issues table
makoMakoGo Jul 14, 2026
48e0d52
fix(health): remove raw diagnostics from public reports
makoMakoGo Jul 14, 2026
8651167
perf(core): eliminate redundant source-cache work
makoMakoGo Jul 14, 2026
c42f9c6
fix(tui): keep the active tab visible on narrow headers
makoMakoGo Jul 14, 2026
4fefead
perf(roocode): avoid discarded rejection diagnostics
makoMakoGo Jul 14, 2026
541998a
test(qwen): remove duplicate rejection assertion
makoMakoGo Jul 14, 2026
c86f8e8
fix(core): correct source health parsing boundaries
makoMakoGo Jul 14, 2026
04d1fb0
fix(omp): bind precomputed parent hashes to snapshots
makoMakoGo Jul 14, 2026
134568a
refactor(cache): clarify single-file snapshot matching
makoMakoGo Jul 14, 2026
7e67ae1
fix(omp): keep parent-derived metadata current
makoMakoGo Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ rayon = "1.10"
# JSON parsing (with SIMD acceleration)
simd-json = "0.14"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = { version = "1.0", features = ["raw_value"] }
bincode = "1.3"

# TOML config parsing
Expand Down
26 changes: 22 additions & 4 deletions crates/tokscale-cli/src/commands/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub(crate) fn run_clients_command(json: bool, home_dir: Option<String>) -> Resul
use tokscale_core::scanner::{
built_in_extra_scan_paths_for, copilot_exporter_path_with_env_strategy,
discover_opencode_dbs, extra_scan_paths_for, opencode_data_dir_with_env_strategy,
parse_extra_dirs,
parse_extra_dirs, ScannerError,
};
use tokscale_core::{
count_local_client_messages, warp_sqlite_roots_with_env_strategy, ClientId,
Expand Down Expand Up @@ -40,6 +40,7 @@ pub(crate) fn run_clients_command(json: bool, home_dir: Option<String>) -> Resul
scanner_settings: scanner_settings.clone(),
})
.map_err(|e| anyhow::anyhow!(e))?;
let mut health = client_counts.health.clone();

let headless_roots =
tokscale_core::scanner::headless_roots_with_env_strategy(&home_dir, use_env_roots);
Expand Down Expand Up @@ -113,11 +114,24 @@ pub(crate) fn run_clients_command(json: bool, home_dir: Option<String>) -> Resul
} else {
Vec::new()
};
let built_in_extra_paths = built_in_extra_scan_paths_for(&home_dir, &all_clients)?;
let built_in_extra_paths = match built_in_extra_scan_paths_for(&home_dir, &all_clients) {
Ok(paths) => paths,
Err(ScannerError::ClaudeMirror(_)) => {
health.record_unavailable_source(ClientId::Claude.as_str());
vec![(ClientId::Claude, home_dir.join(".claude/transcripts"))]
}
Err(error) => return Err(error.into()),
};
let settings_extra_dirs = extra_scan_paths_for(&scanner_settings, &all_clients)?;
let copilot_exporter_path = copilot_exporter_path_with_env_strategy(use_env_roots);
let opencode_data_root = opencode_data_dir_with_env_strategy(&home_dir_str, use_env_roots);
let opencode_auto_dbs = discover_opencode_dbs(&opencode_data_root)?;
let opencode_auto_dbs = match discover_opencode_dbs(&opencode_data_root) {
Ok(paths) => paths,
Err(_) => {
health.record_unavailable_source(ClientId::OpenCode.as_str());
Vec::new()
}
};

let clients: Vec<ClientRow> =
ClientId::iter()
Expand Down Expand Up @@ -269,13 +283,16 @@ pub(crate) fn run_clients_command(json: bool, home_dir: Option<String>) -> Resul
})
.collect();

crate::commands::shared::emit_health_summary(&health);

if json {
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct Output {
struct Output<'a> {
headless_roots: Vec<String>,
clients: Vec<ClientRow>,
note: String,
health: &'a tokscale_core::source_health::HealthReport,
}

let output = Output {
Expand All @@ -285,6 +302,7 @@ pub(crate) fn run_clients_command(json: bool, home_dir: Option<String>) -> Resul
.collect(),
clients,
note: "Headless capture is supported for Codex CLI only.".to_string(),
health: &health,
};

println!("{}", serde_json::to_string_pretty(&output)?);
Expand Down
53 changes: 53 additions & 0 deletions crates/tokscale-cli/src/commands/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub(crate) struct GraphExportData {
contributions: Vec<GraphDailyContribution>,
#[serde(skip_serializing_if = "Option::is_none")]
time_metrics: Option<GraphTimeMetrics>,
health: tokscale_core::source_health::HealthReport,
}

pub(crate) fn to_graph_export_data(graph: &tokscale_core::GraphResult) -> GraphExportData {
Expand Down Expand Up @@ -187,6 +188,7 @@ pub(crate) fn to_graph_export_data(graph: &tokscale_core::GraphResult) -> GraphE
max_concurrent_sessions: tm.max_concurrent_sessions,
session_count: tm.session_count,
}),
health: graph.health.clone(),
}
}

Expand Down Expand Up @@ -242,6 +244,7 @@ pub(crate) fn run_graph_command(
had_cursor_cache,
explicit_cursor_filter,
);
super::shared::emit_health_summary(&graph_result.health);
emit_cursor_setup_warnings(&cursor_setup_warnings);

let processing_time_ms = start.elapsed().as_millis() as u32;
Expand Down Expand Up @@ -302,3 +305,53 @@ pub(crate) fn run_graph_command(

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn graph_export_includes_data_health() {
let graph = tokscale_core::GraphResult {
meta: tokscale_core::GraphMeta {
generated_at: "2026-07-14T00:00:00Z".to_string(),
version: "test".to_string(),
date_range_start: "2026-07-14".to_string(),
date_range_end: "2026-07-14".to_string(),
processing_time_ms: 0,
},
summary: tokscale_core::DataSummary {
total_tokens: 0,
total_cost: 0.0,
total_days: 0,
active_days: 0,
average_per_day: 0.0,
max_cost_in_single_day: 0.0,
clients: Vec::new(),
models: Vec::new(),
},
years: Vec::new(),
contributions: Vec::new(),
time_metrics: None,
health: tokscale_core::source_health::HealthReport {
complete: false,
clean_sources: 4,
degraded_sources: 1,
rejected_records: 2,
partial_sources: 1,
failed_sources: 0,
source_data_bytes: 12_345,
issues: Vec::new(),
},
};

let json = serde_json::to_value(to_graph_export_data(&graph)).unwrap();

assert_eq!(json["health"]["complete"], false);
assert_eq!(json["health"]["cleanSources"], 4);
assert_eq!(json["health"]["degradedSources"], 1);
assert_eq!(json["health"]["rejectedRecords"], 2);
assert_eq!(json["health"]["partialSources"], 1);
assert_eq!(json["health"]["sourceDataBytes"], 12_345);
}
}
3 changes: 3 additions & 0 deletions crates/tokscale-cli/src/commands/hourly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub(crate) fn run_hourly_report(
had_cursor_cache,
explicit_cursor_filter,
);
super::shared::emit_health_summary(&report.health);

let processing_time_ms = start.elapsed().as_millis();

Expand Down Expand Up @@ -106,6 +107,7 @@ pub(crate) fn run_hourly_report(
processing_time_ms: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
warnings: Vec<String>,
health: tokscale_core::source_health::HealthReport,
}

let output = HourlyReportJson {
Expand All @@ -128,6 +130,7 @@ pub(crate) fn run_hourly_report(
total_cost: report.total_cost,
processing_time_ms: report.processing_time_ms,
warnings: cursor_setup_warnings,
health: report.health.clone(),
};

println!("{}", serde_json::to_string_pretty(&output)?);
Expand Down
4 changes: 4 additions & 0 deletions crates/tokscale-cli/src/commands/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub(crate) fn run_models_report(
had_cursor_cache,
explicit_cursor_filter,
);
super::shared::emit_health_summary(&report.health);
let processing_time_ms = start.elapsed().as_millis();
let claude_message_count = report
.entries
Expand Down Expand Up @@ -180,8 +181,10 @@ pub(crate) fn run_models_report(
warnings: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
diagnostics: Vec<claude_diagnostics::ClientDiagnostic>,
health: tokscale_core::source_health::HealthReport,
}

let health = report.health.clone();
let output = ModelReportJson {
group_by: group_by.to_string(),
entries: report
Expand Down Expand Up @@ -234,6 +237,7 @@ pub(crate) fn run_models_report(
processing_time_ms: report.processing_time_ms,
warnings: cursor_setup_warnings,
diagnostics,
health,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
Expand Down
3 changes: 3 additions & 0 deletions crates/tokscale-cli/src/commands/monthly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub(crate) fn run_monthly_report(
had_cursor_cache,
explicit_cursor_filter,
);
super::shared::emit_health_summary(&report.health);

let processing_time_ms = start.elapsed().as_millis();

Expand All @@ -113,6 +114,7 @@ pub(crate) fn run_monthly_report(
processing_time_ms: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
warnings: Vec<String>,
health: tokscale_core::source_health::HealthReport,
}

let output = MonthlyReportJson {
Expand All @@ -136,6 +138,7 @@ pub(crate) fn run_monthly_report(
total_cost: report.total_cost,
processing_time_ms: report.processing_time_ms,
warnings: cursor_setup_warnings,
health: report.health.clone(),
};

println!("{}", serde_json::to_string_pretty(&output)?);
Expand Down
20 changes: 20 additions & 0 deletions crates/tokscale-cli/src/commands/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,23 @@ pub(crate) fn get_date_range_label_for_date(
Some(parts.join(" "))
}
}

/// Print the report's data-health summary to stderr. Data stays on stdout;
/// degraded sources are warnings, never a failed exit.
pub(crate) fn emit_health_summary(health: &tokscale_core::source_health::HealthReport) {
use colored::Colorize;
if health.complete {
return;
}
eprintln!(
"{}",
format!(
" Data health: {} degraded source(s), {} rejected record(s), {} partial source(s), {} failed source(s)",
health.degraded_sources,
health.rejected_records,
health.partial_sources,
health.failed_sources
)
.yellow()
);
}
3 changes: 3 additions & 0 deletions crates/tokscale-cli/src/commands/time_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub(crate) fn run_time_metrics_report(
had_cursor_cache,
explicit_cursor_filter,
);
super::shared::emit_health_summary(&report.health);

let m = &report.metrics;

Expand All @@ -67,12 +68,14 @@ pub(crate) fn run_time_metrics_report(
processing_time_ms: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
warnings: Vec<String>,
health: &'a tokscale_core::source_health::HealthReport,
}

let output = TimeMetricsReportJson {
metrics: &report.metrics,
processing_time_ms: report.processing_time_ms,
warnings: cursor_setup_warnings,
health: &report.health,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
Expand Down
35 changes: 35 additions & 0 deletions crates/tokscale-cli/src/commands/wrapped.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub struct WrappedOptions {

#[derive(Debug, Clone)]
struct WrappedData {
health: tokscale_core::source_health::HealthReport,
year: String,
active_days: i32,
total_tokens: i64,
Expand Down Expand Up @@ -127,6 +128,7 @@ pub fn run(options: WrappedOptions) -> Result<String> {

async fn generate_wrapped(options: WrappedOptions) -> Result<String> {
let data = load_wrapped_data(&options).await?;
crate::commands::shared::emit_health_summary(&data.health);

let agents_requested = options.include_agents;
let has_agent_data = data
Expand Down Expand Up @@ -269,6 +271,7 @@ async fn load_wrapped_data(options: &WrappedOptions) -> Result<WrappedData> {
Some(pricing.as_ref()),
)
.map_err(anyhow::Error::msg)?;
let health = wrapped_health_report(&aggregated);
let graph = aggregated.graph.expect("graph view requested");

let mut model_map: HashMap<String, WrappedRankedEntry> = HashMap::new();
Expand Down Expand Up @@ -329,6 +332,7 @@ async fn load_wrapped_data(options: &WrappedOptions) -> Result<WrappedData> {
.unwrap_or_else(|| format!("{}-01-01", year));

Ok(WrappedData {
health,
year,
active_days: graph.summary.active_days,
total_tokens: graph.summary.total_tokens,
Expand All @@ -342,6 +346,12 @@ async fn load_wrapped_data(options: &WrappedOptions) -> Result<WrappedData> {
})
}

fn wrapped_health_report(
aggregated: &tokscale_core::AggregatedViews,
) -> tokscale_core::source_health::HealthReport {
aggregated.health.to_report()
}

fn accumulate_wrapped_contribution(
model_map: &mut HashMap<String, WrappedRankedEntry>,
client_map: &mut HashMap<String, WrappedRankedEntry>,
Expand Down Expand Up @@ -1718,6 +1728,7 @@ mod tests {
use serial_test::serial;
use std::env;
use tempfile::TempDir;
use tokscale_core::{DataHealth, RejectionSummary, SourceFailure, SourceHealth, SourceStatus};

fn restore_env_var(key: &str, value: Option<std::ffi::OsString>) {
unsafe {
Expand All @@ -1728,6 +1739,30 @@ mod tests {
}
}

#[test]
fn wrapped_health_report_preserves_failed_sources() {
let mut health = DataHealth::default();
health.record(SourceHealth {
client: ClientId::OpenCode,
path: PathBuf::from("/tmp/broken-opencode.db"),
status: SourceStatus::Unavailable {
failure: SourceFailure::new("open database", "invalid database"),
},
rejections: RejectionSummary::default(),
});
let aggregated = tokscale_core::AggregatedViews {
health,
..Default::default()
};

let report = wrapped_health_report(&aggregated);

assert!(!report.complete);
assert_eq!(report.failed_sources, 1);
assert_eq!(report.issues[0].source, "opencode");
Comment thread
makoMakoGo marked this conversation as resolved.
assert_eq!(report.issues[0].issue, "source-unavailable");
}

// ========== format_tokens_short tests ==========

#[test]
Expand Down
Loading
Loading