use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result};
use rusqlite::{Connection, OpenFlags};
use serde::{Deserialize, Serialize};
use crate::db;
use crate::handoff::{self, BranchMode, CheckoutStateView, GitState};
use crate::output::{self, CommandReport, WorkStreamView};
use crate::paths::state::StateLayout;
use crate::profile::{self, ProfileName, DEFAULT_PROFILE};
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::state::escalation as escalation_state;
use crate::state::machine_presence;
use crate::state::pod_identity;
use crate::state::runtime::RuntimeHandoffState;
use crate::state::session as session_state;
use crate::state::session_gates;
use crate::telemetry::{cost as telemetry_cost, host as host_telemetry};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
enum StatusPhase {
BootstrapRequired,
StartRequired,
ReadyForSession,
SessionStale,
SessionActive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
enum ReviewStatus {
Idle,
Available,
Recommended,
AttentionRequired,
}
#[derive(Serialize)]
pub struct StatusReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
#[serde(skip_serializing_if = "Option::is_none")]
project_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
locality_id: Option<String>,
workspace_path: String,
work_stream: WorkStreamView,
phase: StatusPhase,
phase_reason: String,
blocked: bool,
#[serde(skip_serializing_if = "Option::is_none")]
recommended_next_command: Option<String>,
bootstrap: BootstrapState,
pod_identity: pod_identity::PodIdentityView,
machine_identity: pod_identity::MachineIdentityView,
machine_presence: machine_presence::MachinePresenceView,
execution_context: machine_presence::MachineExecutionContextView,
takeover_preconditions: machine_presence::MachineTakeoverPreconditionsView,
coordination_scope: pod_identity::CoordinationScopeView,
git: Option<GitState>,
checkout_state: Option<CheckoutStateView>,
continuity: ContinuityView,
session: StatusSessionView,
execution_gates: session_gates::ExecutionGatesView,
escalation: escalation_state::EscalationView,
telemetry: StatusTelemetryView,
review: StatusReviewView,
warnings: Vec<String>,
}
#[derive(Serialize)]
struct BootstrapState {
status: &'static str,
profile_ready: bool,
linked: bool,
registry_ready: bool,
blockers: Vec<String>,
}
#[derive(Serialize)]
struct ContinuityView {
path: String,
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
immediate_actions: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
definition_of_done: Vec<String>,
}
#[derive(Serialize)]
struct StatusSessionView {
path: String,
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<session_state::SessionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
started_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
last_started_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
start_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
session_minutes: Option<u64>,
#[serde(flatten)]
lifecycle: session_state::SessionLifecycleProjection,
}
#[derive(Serialize)]
struct StatusTelemetryView {
status: &'static str,
#[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")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
context_used_pct: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
total_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
context_window_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
compacted: Option<bool>,
cost: telemetry_cost::TelemetryCostView,
}
#[derive(Serialize)]
struct StatusReviewView {
status: ReviewStatus,
summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
recommended_command: Option<String>,
}
struct ReadOnlyState<T> {
path: PathBuf,
value: Option<T>,
}
enum ReadOnlyDbState<T> {
MissingTable,
Loaded(T),
}
#[derive(Deserialize)]
struct LegacyCloneRuntimeState {
handoff: RuntimeHandoffState,
}
impl CommandReport for StatusReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
println!("Status: {}.", phase_label(self.phase));
println!("{}", self.phase_reason);
if !self.bootstrap.blockers.is_empty() {
println!("Bootstrap blockers:");
for blocker in &self.bootstrap.blockers {
println!("- {blocker}");
}
}
if let Some(title) = &self.continuity.title {
println!("Current focus: {title}");
}
if !self.continuity.immediate_actions.is_empty() {
println!("Next actions:");
for (index, step) in self.continuity.immediate_actions.iter().take(3).enumerate() {
println!("{}. {step}", index + 1);
}
}
if let Some(git) = &self.git {
println!("Workspace: {}", summarize_git_workspace(git));
} else {
println!("Workspace: unavailable by design for directory-substrate workspaces");
}
if let Some(checkout_state) = &self.checkout_state {
if checkout_state.advisory {
println!("Workspace note: {}", checkout_state.summary);
}
}
match &self.recommended_next_command {
Some(command) => println!("Next CCD command: {command}"),
None => println!("Next CCD command: none required right now"),
}
for warning in &self.warnings {
println!("Warning: {warning}");
}
}
}
fn summarize_git_workspace(git: &GitState) -> String {
let mut summary = format!("{} at {}", git.branch, git.head);
let tracking = match (git.upstream.as_deref(), git.ahead, git.behind) {
(Some(upstream), 0, 0) => format!("tracking {upstream} in sync"),
(Some(upstream), ahead, 0) => format!("tracking {upstream}, ahead {ahead}"),
(Some(upstream), 0, behind) => format!("tracking {upstream}, behind {behind}"),
(Some(upstream), ahead, behind) => {
format!("tracking {upstream}, ahead {ahead}, behind {behind}")
}
(None, _, _) => "no upstream".to_owned(),
};
summary.push_str(&format!(" ({tracking})"));
if git.clean {
summary.push_str(", clean");
} else {
summary.push_str(", dirty");
}
summary
}
pub fn run(repo_root: &Path, explicit_profile: Option<&str>) -> Result<StatusReport> {
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve(repo_root, profile.clone())?;
let marker = repo_marker::load(repo_root)?;
let locality_id = marker.as_ref().map(|value| value.locality_id.clone());
let pod_identity = pod_identity::resolve_pod_identity_view(&layout)?;
let machine_identity = pod_identity::resolve_machine_identity_view(&layout)?;
let machine_presence = machine_presence::resolve_machine_presence_view(&layout)?;
let execution_context = machine_presence::build_execution_context_view(
&layout,
locality_id.as_deref(),
&machine_identity,
&machine_presence,
None,
);
let takeover_preconditions = machine_presence::build_takeover_preconditions_view(
&execution_context,
None,
false,
0,
true,
);
let coordination_scope = match locality_id.as_deref() {
Some(locality_id) => pod_identity::resolve_coordination_scope_view(&layout, locality_id)?,
None => pod_identity::CoordinationScopeView {
status: "missing",
name: None,
source: None,
config_path: None,
shared_root: None,
},
};
let bootstrap = build_bootstrap_state(
repo_root,
&layout,
&profile,
explicit_profile,
locality_id.as_deref(),
)?;
let git = if layout.resolved_substrate().is_git() {
handoff::read_git_state(repo_root, BranchMode::AllowDetachedHead).ok()
} else {
None
};
let checkout_state = git
.as_ref()
.map(|git| handoff::checkout_state_view(repo_root, git));
let default_execution_gates = session_gates::build_view(&layout, None);
let default_escalation = escalation_state::build_view(&layout, &[]);
if bootstrap.status == "blocked" {
let phase_reason = bootstrap
.blockers
.first()
.cloned()
.unwrap_or_else(|| "CCD bootstrap is incomplete".to_owned());
let warnings = checkout_state
.as_ref()
.filter(|state| state.advisory)
.map(|state| vec![state.summary.clone()])
.unwrap_or_default();
return Ok(StatusReport {
command: "status",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_string(),
project_id: locality_id.clone(),
locality_id: locality_id.clone(),
workspace_path: repo_root.display().to_string(),
work_stream: output::work_stream_view(git.as_ref()),
phase: StatusPhase::BootstrapRequired,
phase_reason,
blocked: true,
recommended_next_command: recommended_bootstrap_command(
locality_id.as_deref(),
&profile,
explicit_profile,
bootstrap.profile_ready,
bootstrap.linked,
bootstrap.registry_ready,
),
bootstrap,
pod_identity,
machine_identity,
machine_presence,
execution_context,
takeover_preconditions,
coordination_scope,
git,
checkout_state,
continuity: ContinuityView {
path: layout.state_db_path().display().to_string(),
status: "missing",
title: None,
immediate_actions: Vec::new(),
definition_of_done: Vec::new(),
},
session: StatusSessionView {
path: layout.state_db_path().display().to_string(),
status: "missing",
session_id: None,
mode: None,
started_at_epoch_s: None,
last_started_at_epoch_s: None,
start_count: None,
session_minutes: None,
lifecycle: session_state::SessionLifecycleProjection::missing(),
},
execution_gates: default_execution_gates,
escalation: default_escalation,
telemetry: StatusTelemetryView {
status: "unavailable",
host: None,
observed_at_epoch_s: None,
model: None,
context_used_pct: None,
total_tokens: None,
context_window_tokens: None,
compacted: None,
cost: telemetry_cost::TelemetryCostView::unavailable(
"bootstrap_blocked",
"bootstrap must be repaired before telemetry can be summarized",
),
},
review: StatusReviewView {
status: ReviewStatus::Idle,
summary: "status is still in bootstrap mode; close-out review is not available yet"
.to_owned(),
recommended_command: None,
},
warnings,
});
}
let locality_id = locality_id.expect("bootstrap_ready implies locality_id");
let continuity_state = read_handoff_state_read_only(&layout)?;
let continuity = build_continuity_view(&continuity_state.path, continuity_state.value.as_ref());
let now_epoch_s = session_state::now_epoch_s()?;
let tracked_session = read_session_state_read_only(&layout)?;
let tracked_activity = session_state::load_activity_for_layout(&layout)?;
let session = build_session_view(
&tracked_session.path,
tracked_session.value.as_ref(),
tracked_activity.as_ref(),
now_epoch_s,
);
let machine_presence = machine_presence::resolve_machine_presence_view(&layout)?;
let execution_context = machine_presence::build_execution_context_view(
&layout,
Some(&locality_id),
&machine_identity,
&machine_presence,
Some(&session.lifecycle),
);
let execution_gates =
session_gates::build_view(&layout, read_execution_gates_state_read_only(&layout)?);
let escalation_entries = read_escalations_read_only(&layout)?;
let escalation = escalation_state::build_view(&layout, &escalation_entries);
let takeover_preconditions = machine_presence::build_takeover_preconditions_view(
&execution_context,
Some(&session.lifecycle),
escalation.blocking_count > 0,
escalation.blocking_count,
true,
);
let host_snapshot = host_telemetry::current(repo_root).ok().flatten();
let telemetry = build_telemetry_view(
&layout,
&locality_id,
session.session_id.as_deref(),
host_snapshot.as_ref(),
&continuity,
&execution_gates,
)?;
let (phase, phase_reason, recommended_next_command) = derive_phase(
&profile,
explicit_profile,
continuity.status,
session.status,
);
let review = build_review_view(
&profile,
explicit_profile,
git.as_ref(),
&session,
&escalation,
&continuity,
);
let mut warnings = Vec::new();
if let Some(checkout_state) = checkout_state.as_ref().filter(|state| state.advisory) {
warnings.push(checkout_state.summary.clone());
}
if session.status == "stale" {
warnings.push(format!(
"session telemetry at {} is stale; refresh it before relying on long-session context health",
session.path
));
}
if escalation.blocking_count > 0 {
warnings.push(format!(
"{} blocking escalation(s) are active in workspace-local state",
escalation.blocking_count
));
}
Ok(StatusReport {
command: "status",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_string(),
project_id: Some(locality_id.clone()),
locality_id: Some(locality_id),
workspace_path: repo_root.display().to_string(),
work_stream: output::work_stream_view(git.as_ref()),
phase,
phase_reason,
blocked: false,
recommended_next_command,
bootstrap,
pod_identity,
machine_identity,
machine_presence,
execution_context,
takeover_preconditions,
coordination_scope,
git,
checkout_state,
continuity,
session,
execution_gates,
escalation,
telemetry,
review,
warnings,
})
}
fn build_bootstrap_state(
repo_root: &Path,
layout: &StateLayout,
profile: &ProfileName,
explicit_profile: Option<&str>,
locality_id: Option<&str>,
) -> Result<BootstrapState> {
let profile_ready = layout.profile_root().is_dir();
let linked = locality_id.is_some();
let registry_ready = match locality_id {
Some(locality_id) => {
repo_registry::load(&layout.repo_metadata_path(locality_id)?)?.is_some()
}
None => false,
};
let mut blockers = Vec::new();
let attach_command = with_profile_flag("ccd attach --path .", profile, explicit_profile);
let link_command = with_profile_flag("ccd link --path .", profile, explicit_profile);
if !profile_ready {
blockers.push(format!(
"ccd status cannot inspect continuity or workspace-health state because profile `{}` is not bootstrapped at {}. Run `{attach_command}` first.",
profile,
layout.profile_root().display(),
));
}
if !linked {
blockers.push(format!(
"ccd status cannot inspect continuity or workspace-health state because {} is missing. Run `{attach_command}` to bootstrap a new project overlay, or `{link_command}` to reconnect to an existing one.",
repo_root.join(repo_marker::MARKER_FILE).display(),
));
}
if let Some(locality_id) = locality_id {
if !registry_ready {
blockers.push(format!(
"ccd status found project ID `{locality_id}` (`locality_id` compatibility) is not linked in the registry; continuity and workspace-health state may refer to an unregistered project. Run `{}` first.",
with_profile_flag(
&format!("ccd link --path . --project-id {locality_id}"),
profile,
explicit_profile
)
));
}
}
Ok(BootstrapState {
status: if blockers.is_empty() {
"ready"
} else {
"blocked"
},
profile_ready,
linked,
registry_ready,
blockers,
})
}
fn recommended_bootstrap_command(
locality_id: Option<&str>,
profile: &ProfileName,
explicit_profile: Option<&str>,
profile_ready: bool,
linked: bool,
registry_ready: bool,
) -> Option<String> {
if !profile_ready || !linked {
return Some(with_profile_flag(
"ccd attach --path .",
profile,
explicit_profile,
));
}
if !registry_ready {
return locality_id.map(|locality_id| {
with_profile_flag(
&format!("ccd link --path . --project-id {locality_id}"),
profile,
explicit_profile,
)
});
}
None
}
fn build_continuity_view(path: &Path, handoff: Option<&RuntimeHandoffState>) -> ContinuityView {
let handoff = handoff.cloned().unwrap_or_default();
let loaded = !handoff.title.is_empty()
|| !handoff.immediate_actions.is_empty()
|| !handoff.completed_state.is_empty()
|| !handoff.operational_guardrails.is_empty()
|| !handoff.key_files.is_empty()
|| !handoff.definition_of_done.is_empty();
ContinuityView {
path: path.display().to_string(),
status: if loaded { "loaded" } else { "missing" },
title: (!handoff.title.is_empty()).then_some(handoff.title.clone()),
immediate_actions: if loaded {
handoff
.immediate_actions
.iter()
.map(|item| item.text.clone())
.collect()
} else {
Vec::new()
},
definition_of_done: if loaded {
handoff
.definition_of_done
.iter()
.map(|item| item.text.clone())
.collect()
} else {
Vec::new()
},
}
}
fn build_session_view(
path: &Path,
tracked_session: Option<&session_state::SessionStateFile>,
tracked_activity: Option<&session_state::SessionActivityState>,
now_epoch_s: u64,
) -> StatusSessionView {
let Some(state) = tracked_session else {
return StatusSessionView {
path: path.display().to_string(),
status: "missing",
session_id: None,
mode: None,
started_at_epoch_s: None,
last_started_at_epoch_s: None,
start_count: None,
session_minutes: None,
lifecycle: session_state::SessionLifecycleProjection::missing(),
};
};
let lifecycle = session_state::lifecycle_projection(state, now_epoch_s, None, tracked_activity);
let stale = lifecycle.stale == Some(true) || state.session_id.is_none();
let status = if stale { "stale" } else { "active" };
StatusSessionView {
path: path.display().to_string(),
status,
session_id: (!stale).then(|| state.session_id.clone()).flatten(),
mode: Some(state.mode),
started_at_epoch_s: Some(state.started_at_epoch_s),
last_started_at_epoch_s: Some(state.last_started_at_epoch_s),
start_count: Some(state.start_count),
session_minutes: Some(session_state::session_minutes(state, now_epoch_s)),
lifecycle,
}
}
fn build_telemetry_view(
layout: &StateLayout,
locality_id: &str,
active_session_id: Option<&str>,
host_snapshot: Option<&host_telemetry::HostContextSnapshot>,
continuity: &ContinuityView,
execution_gates: &session_gates::ExecutionGatesView,
) -> Result<StatusTelemetryView> {
let focus = telemetry_cost::continuity_target(
execution_gates.attention_anchor.as_ref(),
continuity.title.as_deref().unwrap_or(""),
&continuity.immediate_actions,
);
let cost = telemetry_cost::build_cost_view_for_focus(
layout,
locality_id,
active_session_id,
host_snapshot,
focus.as_ref(),
)?;
let Some(host_snapshot) = host_snapshot else {
return Ok(StatusTelemetryView {
status: "unavailable",
host: None,
observed_at_epoch_s: None,
model: None,
context_used_pct: None,
total_tokens: None,
context_window_tokens: None,
compacted: None,
cost,
});
};
Ok(StatusTelemetryView {
status: "observed",
host: Some(host_snapshot.host),
observed_at_epoch_s: Some(host_snapshot.observed_at_epoch_s),
model: host_snapshot.model_name.clone(),
context_used_pct: host_snapshot.context_used_pct,
total_tokens: host_snapshot.total_tokens,
context_window_tokens: host_snapshot.model_context_window,
compacted: host_snapshot.compacted,
cost,
})
}
fn derive_phase(
profile: &ProfileName,
explicit_profile: Option<&str>,
continuity_status: &str,
session_status: &str,
) -> (StatusPhase, String, Option<String>) {
match continuity_status {
"missing" => (
StatusPhase::StartRequired,
"workspace-local continuity is missing; run `ccd start --activate` to load the handoff and mark session telemetry active in one step".to_owned(),
Some(with_profile_flag(
"ccd start --activate --path .",
profile,
explicit_profile,
)),
),
_ => match session_status {
"missing" => (
StatusPhase::ReadyForSession,
"continuity is loaded, but no active session telemetry is present in this workspace"
.to_owned(),
Some(with_profile_flag(
"ccd session-state start --path .",
profile,
explicit_profile,
)),
),
"stale" => (
StatusPhase::SessionStale,
"continuity is loaded, but session telemetry is stale or missing a durable session id".to_owned(),
Some(with_profile_flag(
"ccd session-state start --path .",
profile,
explicit_profile,
)),
),
_ => (
StatusPhase::SessionActive,
"continuity is loaded and session telemetry is active in this clone".to_owned(),
None,
),
},
}
}
fn build_review_view(
profile: &ProfileName,
explicit_profile: Option<&str>,
git: Option<&GitState>,
session: &StatusSessionView,
escalation: &escalation_state::EscalationView,
continuity: &ContinuityView,
) -> StatusReviewView {
let radar_command = with_profile_flag("ccd radar-state --path .", profile, explicit_profile);
if continuity.status == "missing" {
return StatusReviewView {
status: ReviewStatus::Idle,
summary: "close-out review is not available until continuity has been loaded with `ccd start`".to_owned(),
recommended_command: None,
};
}
if escalation.blocking_count > 0 {
return StatusReviewView {
status: ReviewStatus::AttentionRequired,
summary: format!(
"{} blocking escalation(s) are active; human resolution is required before treating the session as clean",
escalation.blocking_count
),
recommended_command: Some(radar_command),
};
}
if git.is_some_and(|git| !git.clean) {
return StatusReviewView {
status: ReviewStatus::Recommended,
summary: "the worktree has local changes; run radar before close-out to assess the session delta".to_owned(),
recommended_command: Some(radar_command),
};
}
if session.status == "active" {
return StatusReviewView {
status: ReviewStatus::Available,
summary: "session telemetry is active; run radar when you are ready to close out"
.to_owned(),
recommended_command: Some(radar_command),
};
}
if session.status == "stale" {
return StatusReviewView {
status: ReviewStatus::Available,
summary: "a stale session record exists; refresh session telemetry before relying on radar context-health timing".to_owned(),
recommended_command: Some(radar_command),
};
}
StatusReviewView {
status: ReviewStatus::Available,
summary: "continuity is loaded; run radar when you want a factual close-out assessment"
.to_owned(),
recommended_command: Some(radar_command),
}
}
fn with_profile_flag(
command: &str,
profile: &ProfileName,
explicit_profile: Option<&str>,
) -> String {
if explicit_profile.is_none() && profile.as_str() == DEFAULT_PROFILE {
command.to_owned()
} else {
format!("{command} --profile {}", profile.as_str())
}
}
fn phase_label(phase: StatusPhase) -> &'static str {
match phase {
StatusPhase::BootstrapRequired => "bootstrap required",
StatusPhase::StartRequired => "start required",
StatusPhase::ReadyForSession => "ready for session",
StatusPhase::SessionStale => "session stale",
StatusPhase::SessionActive => "session active",
}
}
fn read_handoff_state_read_only(
layout: &StateLayout,
) -> Result<ReadOnlyState<RuntimeHandoffState>> {
if let Some(conn) = open_state_db_read_only(layout)? {
match read_db_handoff_state(&conn)? {
ReadOnlyDbState::Loaded(handoff) => {
return Ok(ReadOnlyState {
path: layout.state_db_path(),
value: handoff,
});
}
ReadOnlyDbState::MissingTable => {}
}
}
let path = layout.clone_runtime_state_path();
let value = read_legacy_json::<LegacyCloneRuntimeState>(&path)?.map(|legacy| legacy.handoff);
Ok(ReadOnlyState { path, value })
}
fn read_session_state_read_only(
layout: &StateLayout,
) -> Result<ReadOnlyState<session_state::SessionStateFile>> {
if let Some(conn) = open_state_db_read_only(layout)? {
match read_db_session_state(&conn)? {
ReadOnlyDbState::Loaded(session) => {
return Ok(ReadOnlyState {
path: layout.state_db_path(),
value: session,
});
}
ReadOnlyDbState::MissingTable => {}
}
}
let path = layout.session_state_path();
let value = read_legacy_json::<session_state::SessionStateFile>(&path)?;
Ok(ReadOnlyState { path, value })
}
fn read_execution_gates_state_read_only(
layout: &StateLayout,
) -> Result<Option<session_gates::ExecutionGateStateFile>> {
let Some(conn) = open_state_db_read_only(layout)? else {
return Ok(None);
};
match read_db_execution_gates(&conn)? {
ReadOnlyDbState::Loaded(gates) => Ok(gates),
ReadOnlyDbState::MissingTable => Ok(None),
}
}
fn read_escalations_read_only(
layout: &StateLayout,
) -> Result<Vec<escalation_state::EscalationEntry>> {
if let Some(conn) = open_state_db_read_only(layout)? {
match read_db_escalations(&conn)? {
ReadOnlyDbState::Loaded(entries) => return Ok(entries),
ReadOnlyDbState::MissingTable => {}
}
}
Ok(
read_legacy_json::<escalation_state::EscalationStateFile>(&layout.escalation_state_path())?
.map(|legacy| legacy.entries)
.unwrap_or_default(),
)
}
fn open_state_db_read_only(layout: &StateLayout) -> Result<Option<Connection>> {
let path = layout.state_db_path();
if !path.exists() {
return Ok(None);
}
Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_context(|| format!("open state.db read-only: {}", path.display()))
.map(Some)
}
fn read_legacy_json<T>(path: &Path) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de>,
{
let contents = match fs::read_to_string(path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(error).with_context(|| format!("failed to read {}", path.display()));
}
};
let value = serde_json::from_str(&contents)
.with_context(|| format!("failed to parse {}", path.display()))?;
Ok(Some(value))
}
fn read_db_handoff_state(
conn: &Connection,
) -> Result<ReadOnlyDbState<Option<RuntimeHandoffState>>> {
match db::handoff::read(conn) {
Ok(value) => Ok(ReadOnlyDbState::Loaded(value)),
Err(error) if is_missing_table_error(&error) => Ok(ReadOnlyDbState::MissingTable),
Err(error) => Err(error),
}
}
fn read_db_session_state(
conn: &Connection,
) -> Result<ReadOnlyDbState<Option<session_state::SessionStateFile>>> {
match db::session::read(conn) {
Ok(value) => Ok(ReadOnlyDbState::Loaded(value)),
Err(error) if is_missing_table_error(&error) => Ok(ReadOnlyDbState::MissingTable),
Err(error) => Err(error),
}
}
fn read_db_execution_gates(
conn: &Connection,
) -> Result<ReadOnlyDbState<Option<session_gates::ExecutionGateStateFile>>> {
match db::session_gates::read(conn) {
Ok(value) => Ok(ReadOnlyDbState::Loaded(value)),
Err(error) if is_missing_table_error(&error) => Ok(ReadOnlyDbState::MissingTable),
Err(error) => Err(error),
}
}
fn read_db_escalations(
conn: &Connection,
) -> Result<ReadOnlyDbState<Vec<escalation_state::EscalationEntry>>> {
match db::escalation::list(conn) {
Ok(value) => Ok(ReadOnlyDbState::Loaded(value)),
Err(error) if is_missing_table_error(&error) => Ok(ReadOnlyDbState::MissingTable),
Err(error) => Err(error),
}
}
fn is_missing_table_error(error: &anyhow::Error) -> bool {
error
.downcast_ref::<rusqlite::Error>()
.and_then(|db_error| match db_error {
rusqlite::Error::SqliteFailure(_, Some(message)) => {
Some(message.contains("no such table"))
}
_ => None,
})
.unwrap_or(false)
}