use std::path::Path;
use std::process::ExitCode;
use anyhow::Result;
use serde::Serialize;
use crate::handoff::{self, BranchMode};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::escalation as escalation_state;
use crate::state::radar;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
use crate::telemetry::{cost as telemetry_cost, host as host_telemetry, host_loop};
#[derive(Serialize)]
pub(crate) struct TelemetryReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
host_loop: host_loop::HostLoopReportView,
prompt_cost: PromptCostReportView,
#[serde(skip_serializing_if = "Option::is_none")]
projection_telemetry: Option<radar::ProjectionTelemetryView>,
}
#[derive(Serialize)]
struct PromptCostReportView {
#[serde(skip_serializing_if = "Option::is_none")]
host: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
observed_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
total_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
context_window_tokens: Option<u64>,
cost: telemetry_cost::TelemetryCostView,
}
impl CommandReport for TelemetryReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
println!("Host loop telemetry: {}", self.host_loop.status);
if let Some(reason) = &self.host_loop.reason {
println!("{reason}");
}
println!(
"Context emission count: {}",
self.host_loop.summary.context_emission_count
);
if let Some(p95_chars) = self.host_loop.summary.p95_chars {
println!("P95 context chars: {p95_chars}");
}
if let Some(max_chars) = self.host_loop.summary.max_chars {
println!("Max context chars: {max_chars}");
}
println!("Prompt cost status: {}", self.prompt_cost.cost.status);
if let Some(reason) = &self.prompt_cost.cost.reason {
println!("{reason}");
}
}
}
pub(crate) fn run_report(
repo_root: &Path,
explicit_profile: Option<&str>,
limit: usize,
) -> Result<TelemetryReport> {
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve(repo_root, profile.clone())?;
let host_loop = host_loop::build_report(&layout.state_db_path(), limit)?;
let host_snapshot = host_telemetry::current(repo_root).ok().flatten();
let (prompt_cost, projection_telemetry) = match repo_marker::load(repo_root)? {
Some(marker) => {
let git = handoff::read_git_state(repo_root, BranchMode::AllowDetachedHead).ok();
let active_session_id = session_state::load_session_id(&layout)?;
let runtime = runtime_state::load_runtime_state(repo_root, &layout, &marker.locality_id)?;
let tracked_session = session_state::load_for_layout(&layout)?;
let tracked_activity = session_state::load_activity_for_layout(&layout)?;
let session_state_view = radar::build_session_state_view(
&layout,
tracked_session.as_ref(),
tracked_activity.as_ref(),
);
let escalation_entries = escalation_state::load_for_layout(&layout)?;
let escalation_view = escalation_state::build_view(&layout, &escalation_entries);
let recovery = runtime_state::recovery_view(&runtime.recovery);
let continuity_actions = runtime
.state
.handoff
.immediate_actions
.iter()
.filter(|item| item.lifecycle.is_active())
.map(|item| item.text.clone())
.collect::<Vec<_>>();
let focus = telemetry_cost::continuity_target(
runtime.execution_gates.view.attention_anchor.as_ref(),
&runtime.state.handoff.title,
&continuity_actions,
);
let (projection_telemetry, _, _) = radar::build_projection_telemetry(
&layout,
&runtime,
&runtime.execution_gates.view,
&escalation_view,
&recovery,
&git,
&session_state_view,
active_session_id.as_deref(),
host_snapshot.as_ref(),
crate::state::compiled::ProjectionTarget::Session,
)?;
(
PromptCostReportView {
host: host_snapshot.as_ref().map(|snapshot| snapshot.host),
observed_at_epoch_s: host_snapshot
.as_ref()
.map(|snapshot| snapshot.observed_at_epoch_s),
total_tokens: host_snapshot.as_ref().and_then(|snapshot| snapshot.total_tokens),
context_window_tokens: host_snapshot
.as_ref()
.and_then(|snapshot| snapshot.model_context_window),
cost: telemetry_cost::build_cost_view_for_focus(
&layout,
&marker.locality_id,
active_session_id.as_deref(),
host_snapshot.as_ref(),
focus.as_ref(),
)?,
},
Some(projection_telemetry),
)
}
None => (
PromptCostReportView {
host: host_snapshot.as_ref().map(|snapshot| snapshot.host),
observed_at_epoch_s: host_snapshot
.as_ref()
.map(|snapshot| snapshot.observed_at_epoch_s),
total_tokens: host_snapshot.as_ref().and_then(|snapshot| snapshot.total_tokens),
context_window_tokens: host_snapshot
.as_ref()
.and_then(|snapshot| snapshot.model_context_window),
cost: telemetry_cost::TelemetryCostView::unavailable(
"missing_locality",
"workspace is not linked to a CCD project, so prompt-cost focus analysis is unavailable",
),
},
None,
),
};
Ok(TelemetryReport {
command: "telemetry-report",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_string(),
host_loop,
prompt_cost,
projection_telemetry,
})
}