use std::path::Path;
use anyhow::Result;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use crate::ask::{Question, Questions};
use crate::config::Config;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Recovery {
max_deputies: usize,
deputy_agents: Vec<String>,
deputy_fallback: bool,
pub answer_timeout: u64,
}
impl Recovery {
pub fn new(cfg: Option<&Config>, max_deputies: usize) -> Self {
Self {
max_deputies,
deputy_agents: cfg.map_or_else(Vec::new, |c| {
c.agents.iter().map(|a| a.id.clone()).collect()
}),
deputy_fallback: cfg.is_some_and(|c| c.resolve_roles().is_ok()),
answer_timeout: cfg.map_or_else(
|| Config::default().graph.answer_timeout,
|c| c.graph.answer_timeout,
),
}
}
fn can_start(&self, agent: &str) -> bool {
self.max_deputies > 0
&& (self.deputy_fallback
|| (!agent.is_empty() && self.deputy_agents.iter().any(|id| id == agent)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UnavailableReason {
Settled,
DeputyExhausted,
DeputyDisabled,
Deadline,
RecipientUnavailable,
DaemonUnavailable,
}
impl UnavailableReason {
pub fn code(self) -> &'static str {
match self {
Self::Settled => "settled",
Self::DeputyExhausted => "deputy_exhausted",
Self::DeputyDisabled => "deputy_disabled",
Self::Deadline => "deadline",
Self::RecipientUnavailable => "recipient_unavailable",
Self::DaemonUnavailable => "daemon_unavailable",
}
}
fn message(self, ja: bool) -> &'static str {
let (en, jp) = match self {
Self::Settled => (
"This question is already settled.",
"この質問は終了しています。",
),
Self::DeputyExhausted => (
"Free-text replies are unavailable: the deputy has used all its starts.",
"自由文を送信できません。代理エージェントの起動回数を使い切りました。",
),
Self::DeputyDisabled => (
"Free-text replies are unavailable: deputies are disabled or their configuration or agent is unavailable.",
"自由文を送信できません。代理エージェントが無効か、設定またはエージェントを利用できません。",
),
Self::Deadline => (
"Free-text replies are unavailable: the recipient's deadline has passed.",
"自由文を送信できません。受信先の応答期限を過ぎています。",
),
Self::RecipientUnavailable => (
"Free-text replies are unavailable: the asking agent has no resumable session.",
"自由文を送信できません。質問したエージェントのセッションを再開できません。",
),
Self::DaemonUnavailable => (
"Free-text replies are unavailable: no running daemon can recover the recipient.",
"自由文を送信できません。受信先を復旧するデーモンが稼働していません。",
),
};
if ja { jp } else { en }
}
}
#[derive(Debug, thiserror::Error)]
#[error("question_text_unavailable ({code}): {message}", code = .reason.code())]
pub struct TextUnavailable {
pub reason: UnavailableReason,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct Availability {
pub state: &'static str,
pub reason: &'static str,
pub message: String,
#[serde(skip)]
unavailable_reason: Option<UnavailableReason>,
}
impl Availability {
pub fn accepts_text(&self) -> bool {
self.state != "unavailable"
}
}
fn recovering(home: &Path, now: Timestamp, cfg: Option<&Config>) -> Option<Recovery> {
let daemon = crate::daemon::read_status(home)?;
let handover = crate::updater::read_progress(home).is_some_and(|p| {
matches!(
p.stage,
crate::updater::Stage::Parking | crate::updater::Stage::Restarting
) && crate::updater::live_lease(&p, crate::updater::read_lease(home).as_ref(), now)
.is_some()
});
let running = daemon.running(now);
let recovery = daemon.question_recovery?;
if running {
return Some(recovery);
}
if handover && cfg.is_some_and(|c| recovery == Recovery::new(Some(c), c.daemon.max_deputies)) {
Some(recovery)
} else {
None
}
}
pub fn availability(store: &Questions, q: &Question, cfg: Option<&Config>) -> Availability {
let now = Timestamp::now();
let home = store.root().parent().unwrap_or(store.root());
let ja = q.merge_target.as_ref().map_or_else(
|| cfg.is_some_and(|c| c.graph.language == "ja"),
|t| t.language == "ja",
);
let result = |state, reason, en: &str, jp: &str| Availability {
state,
reason,
unavailable_reason: None,
message: if ja { jp } else { en }.to_owned(),
};
let unavailable = |reason: UnavailableReason| Availability {
state: "unavailable",
reason: reason.code(),
message: reason.message(ja).to_owned(),
unavailable_reason: Some(reason),
};
if !q.status.open() {
return unavailable(UnavailableReason::Settled);
}
if store.read_lease(&q.id).is_some_and(|l| l.fresh(now)) {
return result(
"live",
"lease",
if q.waiting_on_agent() {
"Waiting for the agent to reply."
} else {
"An agent is listening."
},
if q.waiting_on_agent() {
"エージェントの返答を待っています。"
} else {
"エージェントが応答を受け取れます。"
},
);
}
let recovery = recovering(home, now, cfg);
let timeout = recovery.as_ref().map_or_else(
|| Config::default().graph.answer_timeout,
|r| r.answer_timeout,
);
if crate::deputy::kind_of(q).is_some() {
if q.deputy
.as_ref()
.is_some_and(|d| d.starts >= crate::deputy::MAX_STARTS)
{
return unavailable(UnavailableReason::DeputyExhausted);
}
let Some(recovery) = recovery.as_ref() else {
return unavailable(UnavailableReason::DaemonUnavailable);
};
if !recovery.can_start(crate::deputy::agent_of(q)) {
return unavailable(UnavailableReason::DeputyDisabled);
}
if !crate::deputy::restart_in_time(q, timeout, now) {
return unavailable(UnavailableReason::Deadline);
}
} else {
let waiter = crate::waiter::Waiter::new(store.clone(), home.to_path_buf(), cfg.cloned());
if q.cwd.is_none() || !waiter.can_resume(q) {
return unavailable(UnavailableReason::RecipientUnavailable);
}
if crate::waiter::decide(q, None, false, timeout, now) == crate::waiter::Action::Expire {
return unavailable(UnavailableReason::Deadline);
}
}
if recovery.is_some() {
result(
"waiting",
"recovery",
"Waiting for automatic delivery: the recipient is starting, restarting, or waiting for a free slot.",
"自動配送待ちです。受信先の起動・再起動、または実行枠の空きを待っています。",
)
} else {
unavailable(UnavailableReason::DaemonUnavailable)
}
}
pub fn require(store: &Questions, q: &Question, cfg: Option<&Config>) -> Result<()> {
let delivery = availability(store, q, cfg);
if let Some(reason) = delivery.unavailable_reason {
return Err(TextUnavailable {
reason,
message: delivery.message,
}
.into());
}
Ok(())
}