use anyhow::Result;
use crate::paths::state::StateLayout;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
use crate::state::session_gates;
use crate::state::work_stream_decay;
use crate::telemetry::{cost as telemetry_cost, host as host_telemetry};
use super::{ContextHealth, ContextSignals};
#[allow(clippy::too_many_arguments)]
pub(super) fn build_context_health(
layout: &StateLayout,
locality_id: &str,
tracked_session: Option<&session_state::SessionStateFile>,
active_session_id: Option<&str>,
host_snapshot: Option<&host_telemetry::HostContextSnapshot>,
handoff: &runtime_state::RuntimeHandoffState,
attention_anchor: Option<&session_gates::ExecutionGateAnchor>,
work_stream_decay: &work_stream_decay::WorkStreamDecayView,
) -> Result<ContextHealth> {
let now_epoch_s = session_state::now_epoch_s()?;
let context_used_pct = host_snapshot
.and_then(|snapshot| snapshot.context_used_pct)
.or_else(|| optional_env_u8("CCD_CONTEXT_USED_PCT"));
let signals = ContextSignals {
compacted: host_snapshot
.and_then(|snapshot| snapshot.compacted)
.or_else(|| optional_env_bool("CCD_CONTEXT_COMPACTED")),
session_minutes: tracked_session
.map(|state| session_state::session_minutes(state, now_epoch_s)),
ccd_start_cycles_in_conversation: tracked_session.map(|state| state.start_count),
host_total_tokens: host_snapshot.and_then(|snapshot| snapshot.total_tokens),
host_context_window_tokens: host_snapshot
.and_then(|snapshot| snapshot.model_context_window),
tool_output_kb: optional_env_u64("CCD_TOOL_OUTPUT_KB"),
operator_symptoms: optional_env_list("CCD_OPERATOR_SYMPTOMS"),
};
let has_host_signal = host_snapshot.is_some();
let has_env_signal = optional_env_u8("CCD_CONTEXT_USED_PCT").is_some()
|| signals.compacted.is_some()
|| signals.tool_output_kb.is_some()
|| !signals.operator_symptoms.is_empty();
let has_session_signal =
signals.session_minutes.is_some() || signals.ccd_start_cycles_in_conversation.is_some();
let source = match (has_host_signal, has_env_signal, has_session_signal) {
(true, _, true) => "mixed",
(true, _, false) => "host",
(false, true, true) => "mixed",
(false, true, false) => "env",
(false, false, true) => "heuristic",
(false, false, false) => "none",
};
let mut risk = 0u8;
let mut scored_signals = 0usize;
if let Some(minutes) = signals.session_minutes {
scored_signals += 1;
risk = risk.saturating_add(match minutes {
0..=29 => 0,
30..=59 => 10,
60..=89 => 25,
_ => 40,
});
}
if let Some(cycles) = signals.ccd_start_cycles_in_conversation {
scored_signals += 1;
risk = risk.saturating_add(match cycles {
0..=1 => 0,
2..=3 => 10,
_ => 25,
});
}
if let Some(used_pct) = context_used_pct {
scored_signals += 1;
risk = risk.saturating_add(match used_pct {
0..=69 => 0,
70..=84 => 10,
85..=94 => 20,
_ => 30,
});
}
if context_used_pct.is_none() {
if let Some(total_tokens) = signals.host_total_tokens {
scored_signals += 1;
risk = risk.saturating_add(match total_tokens {
0..=99_999 => 0,
100_000..=199_999 => 5,
200_000..=399_999 => 15,
400_000..=699_999 => 25,
_ => 35,
});
}
}
if let Some(compacted) = signals.compacted {
scored_signals += 1;
if compacted {
risk = risk.saturating_add(35);
}
}
if let Some(tool_output_kb) = signals.tool_output_kb {
scored_signals += 1;
risk = risk.saturating_add(match tool_output_kb {
0..=99 => 0,
100..=249 => 10,
250..=499 => 20,
_ => 30,
});
}
if !signals.operator_symptoms.is_empty() {
scored_signals += 1;
let symptom_score: u8 = signals.operator_symptoms.iter().take(3).map(|_| 15u8).sum();
risk = risk.saturating_add(symptom_score);
}
if work_stream_decay.consecutive_no_progress > 0 {
scored_signals += 1;
risk = risk.saturating_add(match work_stream_decay.consecutive_no_progress {
0..=1 => 5,
2 => 20,
_ => 35,
});
}
let degradation_risk_pct = if scored_signals == 0 {
None
} else {
Some(risk.min(95))
};
let band = match degradation_risk_pct {
None => "unknown",
Some(0..=34) => "low",
Some(35..=59) => "moderate",
Some(60..=79) => "risky",
Some(_) => "critical",
};
let host_window_unknown = signals.host_total_tokens.is_some()
&& context_used_pct.is_none()
&& signals.host_context_window_tokens.is_none();
let confidence = if host_window_unknown {
"low"
} else if has_host_signal && (signals.compacted.is_some() || has_session_signal) {
"high"
} else if has_host_signal || has_session_signal || has_env_signal {
"medium"
} else {
"low"
};
let recommendation = match degradation_risk_pct {
Some(80..=100) => "wrap_up_and_clear",
Some(60..=79) => "wrap_up_soon",
Some(35..=59) => "keep_focus",
Some(_) if host_window_unknown => "capture_session_state",
Some(_) => "continue",
None if host_window_unknown => "capture_session_state",
None if has_host_signal || has_env_signal || has_session_signal => "continue",
None => "capture_session_state",
};
let recommendation = match work_stream_decay.status {
"critical" => "wrap_up_and_clear",
"warning" if recommendation == "continue" => "wrap_up_soon",
_ => recommendation,
};
let continuity_actions = handoff
.immediate_actions
.iter()
.filter(|item| item.lifecycle.is_active())
.map(|item| item.text.clone())
.collect::<Vec<_>>();
let focus =
telemetry_cost::continuity_target(attention_anchor, &handoff.title, &continuity_actions);
let cost = telemetry_cost::build_cost_view_for_focus(
layout,
locality_id,
active_session_id,
host_snapshot,
focus.as_ref(),
)?;
Ok(ContextHealth {
source,
context_used_pct,
degradation_risk_pct,
band,
confidence,
signals,
cost,
recommendation,
})
}
fn optional_env_bool(name: &str) -> Option<bool> {
let value = std::env::var(name).ok()?;
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" => Some(true),
"0" | "false" | "no" => Some(false),
_ => None,
}
}
fn optional_env_u8(name: &str) -> Option<u8> {
std::env::var(name)
.ok()
.and_then(|value| value.trim().parse::<u8>().ok())
}
fn optional_env_u64(name: &str) -> Option<u64> {
std::env::var(name)
.ok()
.and_then(|value| value.trim().parse::<u64>().ok())
}
fn optional_env_list(name: &str) -> Vec<String> {
std::env::var(name)
.ok()
.map(|value| {
value
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(str::to_owned)
.collect()
})
.unwrap_or_default()
}