use crate::handoff;
use crate::session_boundary::SessionBoundaryAction;
use crate::state::escalation as escalation_state;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
use crate::state::session_gates;
use super::{
BehavioralDriftState, CandidateUpdate, CheckpointResult, ContextCheckAction,
ContextCheckPayload, ContextCheckTrigger, ContextCheckUrgency, ContextHealth,
DriftAggregateStatus, HandoffState, NextStepStatus, RadarSessionStateView, RadarStateReport,
};
pub(super) struct ContextCheckDecision {
pub(super) action: ContextCheckAction,
pub(super) reason: &'static str,
pub(super) recommendation: String,
pub(super) urgency: ContextCheckUrgency,
pub(super) suppressed: bool,
pub(super) next_interval_hint_seconds: u64,
pub(super) evidence: Vec<String>,
}
pub(super) struct ContextCheckInputs<'a> {
pub(super) checkout_state: Option<&'a handoff::CheckoutStateView>,
pub(super) escalation: &'a escalation_state::EscalationView,
pub(super) behavioral_drift: &'a BehavioralDriftState,
pub(super) context_health: &'a ContextHealth,
pub(super) session_boundary_action: SessionBoundaryAction,
pub(super) session_boundary_evidence: &'a [String],
pub(super) next_step_status: NextStepStatus,
pub(super) next_step_summary: String,
pub(super) next_step_evidence: Vec<String>,
pub(super) execution_gates: &'a session_gates::ExecutionGatesView,
pub(super) recovery_status: &'a str,
pub(super) handoff: &'a HandoffState,
pub(super) candidate_updates_handoff: &'a [CandidateUpdate],
pub(super) candidate_updates_memory_count: usize,
pub(super) session_state: &'a RadarSessionStateView,
}
impl<'a> ContextCheckInputs<'a> {
pub(super) fn from_handover(report: &'a RadarStateReport) -> Self {
Self {
checkout_state: report.checkout_state.as_ref(),
escalation: &report.escalation,
behavioral_drift: &report.behavioral_drift,
context_health: &report.context_health,
session_boundary_action: report.session_boundary.action,
session_boundary_evidence: &report.session_boundary.evidence,
next_step_status: report.evaluation.next_step.status,
next_step_summary: report.evaluation.next_step.summary.clone(),
next_step_evidence: report.evaluation.next_step.evidence.clone(),
execution_gates: &report.execution_gates,
recovery_status: report.recovery.status,
handoff: &report.handoff,
candidate_updates_handoff: &report.candidate_updates.handoff,
candidate_updates_memory_count: report.candidate_updates.memory.len(),
session_state: &report.session_state,
}
}
pub(super) fn from_checkpoint(cp: &'a CheckpointResult) -> Self {
let (next_step_status, next_step_summary, next_step_evidence) = if !cp
.candidate_updates
.handoff
.is_empty()
{
(
NextStepStatus::ReviewRequired,
"Repo state changed relative to the recorded handoff; refresh the workspace-local handoff before closing.".to_owned(),
cp.candidate_updates
.handoff
.iter()
.map(|c| c.summary.clone())
.collect(),
)
} else if cp
.execution_gates
.attention_anchor
.as_ref()
.is_some_and(|a| a.status == session_gates::ExecutionGateStatus::Blocked)
{
(
NextStepStatus::ReviewRequired,
"The first unfinished execution gate is blocked.".to_owned(),
Vec::new(),
)
} else if cp.execution_gates.attention_anchor.is_some() {
(
NextStepStatus::Continue,
"Execution gates are active.".to_owned(),
Vec::new(),
)
} else {
(
NextStepStatus::NoChangeDetected,
"No deterministic CLI signal says the next step changed.".to_owned(),
Vec::new(),
)
};
Self {
checkout_state: cp.checkout_state.as_ref(),
escalation: &cp.escalation,
behavioral_drift: &cp.behavioral_drift,
context_health: &cp.context_health,
session_boundary_action: cp.session_boundary.inner.action,
session_boundary_evidence: &cp.session_boundary.inner.evidence,
next_step_status,
next_step_summary,
next_step_evidence,
execution_gates: &cp.execution_gates,
recovery_status: cp.recovery.status,
handoff: &cp.handoff,
candidate_updates_handoff: &cp.candidate_updates.handoff,
candidate_updates_memory_count: 0,
session_state: &cp.session_state,
}
}
}
pub(super) fn decide(
trigger: ContextCheckTrigger,
inputs: &ContextCheckInputs<'_>,
payload: &ContextCheckPayload,
) -> ContextCheckDecision {
if let Some(checkout_state) = inputs.checkout_state.filter(|state| state.advisory) {
return ContextCheckDecision {
action: ContextCheckAction::Escalate,
reason: "checkout_advisory",
recommendation: "seek_operator_attention".to_owned(),
urgency: ContextCheckUrgency::High,
suppressed: false,
next_interval_hint_seconds: 0,
evidence: vec![checkout_state.summary.clone()],
};
}
if inputs.escalation.blocking_count > 0 {
return ContextCheckDecision {
action: ContextCheckAction::Escalate,
reason: "blocking_escalation",
recommendation: "seek_operator_attention".to_owned(),
urgency: ContextCheckUrgency::Critical,
suppressed: false,
next_interval_hint_seconds: 0,
evidence: inputs
.escalation
.entries
.iter()
.filter(|entry| matches!(entry.kind, escalation_state::EscalationKind::Blocking))
.map(|entry| format!("{}: {}", entry.id, entry.reason))
.collect(),
};
}
if inputs.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
return ContextCheckDecision {
action: ContextCheckAction::Escalate,
reason: "behavioral_drift",
recommendation: "recalibrate_session".to_owned(),
urgency: ContextCheckUrgency::High,
suppressed: false,
next_interval_hint_seconds: 0,
evidence: inputs.behavioral_drift.evidence.clone(),
};
}
if matches!(
trigger,
ContextCheckTrigger::PreCompaction | ContextCheckTrigger::IdleReset
) {
let reason = match trigger {
ContextCheckTrigger::PreCompaction => "pre_compaction",
ContextCheckTrigger::IdleReset => "idle_reset",
_ => unreachable!("guarded above"),
};
let mut evidence = vec![payload.risk_summary.clone()];
evidence.extend(inputs.session_boundary_evidence.iter().take(2).cloned());
return ContextCheckDecision {
action: ContextCheckAction::FlushForCompaction,
reason,
recommendation: "capture_before_compaction".to_owned(),
urgency: ContextCheckUrgency::High,
suppressed: false,
next_interval_hint_seconds: 0,
evidence,
};
}
if matches!(
inputs.context_health.recommendation,
"wrap_up_soon" | "wrap_up_and_clear"
) {
return ContextCheckDecision {
action: ContextCheckAction::WrapUpRequired,
reason: "wrap_up_window",
recommendation: inputs.context_health.recommendation.to_owned(),
urgency: match inputs.context_health.recommendation {
"wrap_up_and_clear" => ContextCheckUrgency::Critical,
_ => ContextCheckUrgency::High,
},
suppressed: false,
next_interval_hint_seconds: 0,
evidence: if inputs.session_boundary_evidence.is_empty() {
vec![payload.risk_summary.clone()]
} else {
inputs.session_boundary_evidence.to_vec()
},
};
}
if inputs.session_boundary_action == SessionBoundaryAction::Refresh
|| inputs.next_step_status == NextStepStatus::ReviewRequired
{
return ContextCheckDecision {
action: ContextCheckAction::CheckpointNow,
reason: "handoff_refresh_needed",
recommendation: "checkpoint_now".to_owned(),
urgency: ContextCheckUrgency::Moderate,
suppressed: false,
next_interval_hint_seconds: 0,
evidence: if inputs.next_step_evidence.is_empty() {
vec![payload.state_delta.clone()]
} else {
inputs.next_step_evidence.clone()
},
};
}
let manualish_trigger = matches!(
trigger,
ContextCheckTrigger::Manual
| ContextCheckTrigger::Resume
| ContextCheckTrigger::SupervisorPoll
);
let moderate_pressure = matches!(inputs.context_health.band, "moderate" | "risky")
|| inputs.execution_gates.unfinished_count > 0
|| inputs.recovery_status == "loaded";
if manualish_trigger || moderate_pressure {
return ContextCheckDecision {
action: ContextCheckAction::InjectDelta,
reason: match trigger {
ContextCheckTrigger::Resume => "resume_refresh",
ContextCheckTrigger::SupervisorPoll => "supervisor_poll",
ContextCheckTrigger::Manual => "manual_refresh",
_ => "band_transition",
},
recommendation: match trigger {
ContextCheckTrigger::Resume => "resume_with_digest".to_owned(),
_ => inputs.context_health.recommendation.to_owned(),
},
urgency: if matches!(inputs.context_health.band, "risky") {
ContextCheckUrgency::High
} else {
ContextCheckUrgency::Moderate
},
suppressed: false,
next_interval_hint_seconds: if matches!(trigger, ContextCheckTrigger::Resume) {
300
} else {
600
},
evidence: vec![payload.state_delta.clone(), payload.risk_summary.clone()],
};
}
ContextCheckDecision {
action: ContextCheckAction::None,
reason: if trigger == ContextCheckTrigger::Interval {
"interval_suppressed"
} else {
"no_action"
},
recommendation: "continue".to_owned(),
urgency: ContextCheckUrgency::Low,
suppressed: trigger == ContextCheckTrigger::Interval,
next_interval_hint_seconds: 900,
evidence: vec![payload.risk_summary.clone()],
}
}
#[allow(dead_code)]
pub(super) fn build_context_check_decision(
trigger: ContextCheckTrigger,
report: &RadarStateReport,
payload: &ContextCheckPayload,
) -> ContextCheckDecision {
let inputs = ContextCheckInputs::from_handover(report);
decide(trigger, &inputs, payload)
}
pub(super) fn build_policy_digest(runtime: &runtime_state::LoadedRuntimeState) -> String {
let guardrails: Vec<String> = runtime
.state
.handoff
.operational_guardrails
.iter()
.map(|item| item.text.clone())
.collect();
if guardrails.is_empty() {
return "No compiled operational guardrails are currently recorded.".to_owned();
}
summarize_lines(&guardrails, 2)
}
pub(super) fn build_focus_digest_from_inputs(inputs: &ContextCheckInputs<'_>) -> String {
let mut parts = vec![inputs.handoff.title.clone()];
if let Some(mode) = inputs
.session_state
.mode
.filter(|mode| *mode != session_state::SessionMode::General)
{
parts.push(format!("mode `{}`", mode.as_str()));
}
if let Some(anchor) = &inputs.execution_gates.attention_anchor {
parts.push(format!(
"gate [{} #{}/{}] {}",
anchor.status.as_str(),
anchor.index,
inputs.execution_gates.total_count,
anchor.text
));
} else if let Some(next_action) = inputs.handoff.immediate_actions.first() {
parts.push(format!("next `{next_action}`"));
}
parts.join("; ")
}
#[allow(dead_code)]
pub(super) fn build_focus_digest(report: &RadarStateReport) -> String {
let inputs = ContextCheckInputs::from_handover(report);
build_focus_digest_from_inputs(&inputs)
}
pub(super) fn build_state_delta_from_inputs(inputs: &ContextCheckInputs<'_>) -> String {
let mut parts = Vec::new();
if !inputs.candidate_updates_handoff.is_empty() {
parts.push(format!(
"{} handoff update candidate(s)",
inputs.candidate_updates_handoff.len()
));
}
if inputs.candidate_updates_memory_count > 0 {
parts.push(format!(
"{} memory promotion candidate(s)",
inputs.candidate_updates_memory_count
));
}
if let Some(anchor) = &inputs.execution_gates.attention_anchor {
parts.push(format!(
"execution gate [{} #{}/{}] {}",
anchor.status.as_str(),
anchor.index,
inputs.execution_gates.total_count,
anchor.text
));
}
if parts.is_empty() {
parts.push(inputs.next_step_summary.clone());
}
parts.join("; ")
}
#[allow(dead_code)]
pub(super) fn build_state_delta(report: &RadarStateReport) -> String {
let inputs = ContextCheckInputs::from_handover(report);
build_state_delta_from_inputs(&inputs)
}
pub(super) fn build_risk_summary_from_inputs(
trigger: ContextCheckTrigger,
inputs: &ContextCheckInputs<'_>,
) -> String {
let mut parts = vec![format!(
"trigger `{}` with context band `{}` (`{}`)",
trigger.as_str(),
inputs.context_health.band,
inputs.context_health.recommendation
)];
if inputs.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
parts.push("behavioral drift needs recalibration".to_owned());
}
if inputs.escalation.blocking_count > 0 {
parts.push(format!(
"{} blocking escalation(s) active",
inputs.escalation.blocking_count
));
}
parts.push(format!(
"session boundary is `{}`",
inputs.session_boundary_action.as_str()
));
parts.join("; ")
}
#[allow(dead_code)]
pub(super) fn build_risk_summary(
trigger: ContextCheckTrigger,
report: &RadarStateReport,
) -> String {
let inputs = ContextCheckInputs::from_handover(report);
build_risk_summary_from_inputs(trigger, &inputs)
}
fn summarize_lines(lines: &[String], limit: usize) -> String {
let mut parts: Vec<String> = lines.iter().take(limit).cloned().collect();
if lines.len() > limit {
parts.push(format!("+{} more", lines.len() - limit));
}
parts.join("; ")
}