use std::sync::Arc;
use crate::daemon::api::types::{
AdoptResponse, BreakerEntry, BreakersResponse, BugReportPreview, ErrorSummary, ErrorsResponse,
HealthResponse, LlmChatRequest, LlmChatResponse, OptimizerResponse, OverseerResponse,
OverseerStatus, ReportBugHttpResponse, ScrubChangeSummary, TmuxSessionsResponse,
TmuxSnapshotResponse,
};
use crate::daemon::api::{AdoptRequest, DoctorQuery, ErrorsQuery, ReportBugApiRequest};
use crate::daemon::bug_report;
use crate::daemon::error::DaemonError;
use crate::daemon::services::TmuxService;
use crate::daemon::state::DaemonState;
pub async fn health(state: &Arc<DaemonState>) -> HealthResponse {
let report = super::health_catalog::report(state).await;
HealthResponse {
status: "ok".to_owned(),
catalog_stale: report.stale,
catalog_unknown: report.unknown,
catalog_changes: report.summary_lines(),
supervised: state.supervised(),
launchd_supervision: state.launchd_supervision(),
version: env!("CARGO_PKG_VERSION").to_owned(),
build_id: state.build_identity(),
pid: std::process::id(),
unsupervised_forced: state.unsupervised_forced(),
degraded: crate::core::pr_cleanup::auth_backoff::shared()
.degraded_reason()
.into_iter()
.collect(),
}
}
pub async fn doctor(
state: &Arc<DaemonState>,
query: DoctorQuery,
) -> crate::core::doctor::DoctorReport {
let mgr = state.session_manager().await;
let mut report =
crate::daemon::doctor::run_doctor_for_manager(&mgr, query.project.as_deref()).await;
crate::daemon::doctor::apply_daemon_seal(&mut report, state.session_claudes().sealed());
report
}
pub fn list_errors(state: &Arc<DaemonState>, query: ErrorsQuery) -> ErrorsResponse {
let limit = query.limit.unwrap_or(20).min(100) as usize;
let errors = match state.error_store_base() {
Some(base) => {
bug_report::aggregate_errors_from_paths(&bug_report::store_paths_under(base), limit)
}
None => bug_report::aggregate_errors(limit),
};
let summaries: Vec<ErrorSummary> = errors
.iter()
.map(|e| ErrorSummary {
fingerprint: e.record.fingerprint.clone(),
crate_target: e.record.crate_target.clone(),
crate_version: e.record.crate_version.clone(),
summary: e.record.summary(),
occurrences: e.occurrences,
timestamp_secs: e.record.timestamp_secs,
})
.collect();
let total = summaries.len();
ErrorsResponse {
errors: summaries,
total,
limit,
}
}
pub fn to_wire_preview(p: &bug_report::IssuePreview) -> BugReportPreview {
BugReportPreview {
title: p.title.clone(),
body: p.body.clone(),
labels: p.labels.clone(),
scrub_changes: p
.scrub_changes
.iter()
.map(|c| ScrubChangeSummary {
pattern: c.pattern.to_string(),
hint: c.hint.to_string(),
})
.collect(),
}
}
pub async fn report_bug(
_state: &Arc<DaemonState>,
body: ReportBugApiRequest,
) -> ReportBugHttpResponse {
let errors = bug_report::aggregate_errors(500);
let found = errors
.into_iter()
.find(|e| e.record.fingerprint == body.fingerprint);
let Some(agg) = found else {
return ReportBugHttpResponse {
filed: false,
deduped: None,
issue_url: None,
issue_number: None,
note: Some(format!(
"fingerprint `{}` not found in local error stores; \
run GET /api/v1/errors to see available fingerprints",
body.fingerprint
)),
preview: None,
rate_limited: None,
};
};
let preview = bug_report::build_preview(&agg);
if !body.confirm {
return ReportBugHttpResponse {
filed: false,
deduped: None,
issue_url: None,
issue_number: None,
note: Some("confirm:false — preview only. POST with confirm:true to file.".to_string()),
preview: Some(to_wire_preview(&preview)),
rate_limited: None,
};
}
let guard = bug_report::RateLimitGuard::production();
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let rl_decision = guard.check(&body.fingerprint, now_secs);
if !rl_decision.is_allowed() {
return ReportBugHttpResponse {
filed: false,
deduped: None,
issue_url: None,
issue_number: None,
note: Some(rl_decision.block_reason()),
preview: None,
rate_limited: Some(true),
};
}
let fingerprint = body.fingerprint.clone();
let provider = bug_report::ResolvedProvider;
let result =
tokio::task::spawn_blocking(move || bug_report::file_issue(&preview, &provider)).await;
match result {
Ok(Ok(filing)) => {
guard.record_filed(&fingerprint, now_secs);
ReportBugHttpResponse {
filed: true,
deduped: Some(filing.deduped),
issue_url: Some(filing.issue_url),
issue_number: Some(filing.issue_number),
note: None,
preview: None,
rate_limited: None,
}
}
Ok(Err(bug_report::GithubFilingError::NoToken)) => ReportBugHttpResponse {
filed: false,
deduped: None,
issue_url: None,
issue_number: None,
note: Some(bug_report::GithubFilingError::NoToken.to_string()),
preview: None,
rate_limited: None,
},
Ok(Err(e)) => ReportBugHttpResponse {
filed: false,
deduped: None,
issue_url: None,
issue_number: None,
note: Some(format!("GitHub filing failed: {e}")),
preview: None,
rate_limited: None,
},
Err(e) => ReportBugHttpResponse {
filed: false,
deduped: None,
issue_url: None,
issue_number: None,
note: Some(format!("internal error: {e}")),
preview: None,
rate_limited: None,
},
}
}
pub fn breakers(state: &Arc<DaemonState>) -> BreakersResponse {
let breakers = state
.all_breakers()
.into_iter()
.map(|(agent, breaker)| BreakerEntry { agent, breaker })
.collect();
BreakersResponse { breakers }
}
pub fn overseer(state: &Arc<DaemonState>) -> OverseerResponse {
OverseerResponse {
overseer: OverseerStatus {
enabled: state.overseer().is_enabled(),
handler: state.overseer_handler().to_string(),
},
}
}
pub fn optimizer(state: &Arc<DaemonState>) -> OptimizerResponse {
OptimizerResponse {
optimizer: state.optimizer_config(),
scope: crate::daemon::optimizer::OPTIMIZER_SCOPE_NOTE.to_string(),
}
}
pub async fn llm_chat(
state: &Arc<DaemonState>,
body: LlmChatRequest,
) -> Result<LlmChatResponse, DaemonError> {
let overseer = state.llm_overseer().ok_or_else(|| {
DaemonError::ServiceUnavailable(
"LLM chat is not configured (no OpenRouter API key)".to_string(),
)
})?;
let mut history = body.history;
let reply = overseer
.chat(&mut history, &body.message)
.await
.map_err(|e| DaemonError::Internal(e.to_string()))?;
Ok(LlmChatResponse { reply, history })
}
pub fn list_tmux_sessions(_state: &Arc<DaemonState>) -> TmuxSessionsResponse {
TmuxSessionsResponse {
sessions: TmuxService::list_all(),
}
}
pub fn tmux_snapshot(
_state: &Arc<DaemonState>,
name: &str,
) -> Result<TmuxSnapshotResponse, DaemonError> {
let snapshot = TmuxService::snapshot(name, 100)?;
Ok(TmuxSnapshotResponse { snapshot })
}
pub fn adopt_tmux(
_state: &Arc<DaemonState>,
body: AdoptRequest,
) -> Result<AdoptResponse, DaemonError> {
let adopted = TmuxService::adopt(&body.session)?;
Ok(AdoptResponse { adopted })
}