use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
use chrono::{DateTime, Utc};
use tokio::sync::mpsc::UnboundedReceiver;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::app::events::{AppEvent, BackgroundKind};
use crate::shared::i18n::Locale;
use super::Orchestrator;
use super::compaction::CompactResult;
enum Landing {
Task(BgDone),
Roll(CompactResult),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum BgOutcome {
Done,
Cancelled { consumed: bool },
Failed(String),
}
#[derive(Debug)]
pub(super) struct BgDone {
pub(super) kind: BackgroundKind,
pub(super) outcome: BgOutcome,
pub(super) prefill: Option<crate::shared::api::contract::Prefill>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Window {
Reflection {
chat: Uuid,
upto: Option<usize>,
at: Option<DateTime<Utc>>,
},
Counter { chat: Uuid, count: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub(super) enum Acting {
Idle = 0,
InTools = 1,
Wrote = 2,
}
#[derive(Debug, Default)]
pub(super) struct Acted(AtomicU8);
impl Acted {
#[cfg(test)]
pub(super) fn at(state: Acting) -> Self {
Self(AtomicU8::new(state as u8))
}
pub(super) fn set(&self, state: Acting) {
self.0.store(state as u8, Ordering::SeqCst);
}
pub(super) fn enter_tools(&self, wrote_so_far: bool) {
if !wrote_so_far {
self.set(Acting::InTools);
}
}
pub(super) fn leave_tools(&self, wrote_so_far: bool, round_wrote: bool) -> bool {
let wrote = wrote_so_far || round_wrote;
self.set(if wrote { Acting::Wrote } else { Acting::Idle });
wrote
}
pub(super) fn get(&self) -> Acting {
match self.0.load(Ordering::SeqCst) {
0 => Acting::Idle,
1 => Acting::InTools,
_ => Acting::Wrote,
}
}
}
pub(super) struct Refund {
pub window: Window,
pub acted: Arc<Acted>,
}
#[derive(Default)]
pub(super) struct BgSlot {
cancel: Option<CancellationToken>,
failures: u32,
refund: Option<Refund>,
}
impl Orchestrator {
pub(super) fn bg_running(&self, kind: BackgroundKind) -> bool {
self.bg.get(&kind).is_some_and(|s| s.cancel.is_some())
}
pub(super) fn begin_bg(
&mut self,
kind: BackgroundKind,
cancel: CancellationToken,
refund: Option<Refund>,
) {
let slot = self.bg.entry(kind).or_default();
slot.cancel = Some(cancel);
slot.refund = refund;
let _ = self
.evt_tx
.send(AppEvent::BackgroundTask { kind, active: true });
self.emit_task_list();
}
pub(super) fn handle_bg_done(
&mut self,
kind: BackgroundKind,
outcome: BgOutcome,
prefill: Option<crate::shared::api::contract::Prefill>,
) {
let (alert, window) = {
let slot = self.bg.entry(kind).or_default();
slot.cancel = None;
let window = slot.refund.take().map(|r| r.window);
let alert = match &outcome {
BgOutcome::Done => {
slot.failures = 0;
None
}
BgOutcome::Cancelled { .. } => None,
BgOutcome::Failed(reason) => {
slot.failures += 1;
(slot.failures == super::BACKGROUND_FAILURE_ALERT).then(|| reason.clone())
}
};
(alert, window)
};
if let (Some(window), BgOutcome::Cancelled { consumed: false }) = (window, &outcome) {
self.give_back(kind, window);
}
let _ = self.evt_tx.send(AppEvent::BackgroundTask {
kind,
active: false,
});
self.emit_task_list();
if !matches!(outcome, BgOutcome::Failed(_))
&& matches!(
kind,
BackgroundKind::Reflection | BackgroundKind::SelfConsolidation
)
{
let _ = self.evt_tx.send(AppEvent::SelfModelChanged);
}
if let Some(reason) = alert {
let loc = self.ui_locale();
let _ = self.evt_tx.send(AppEvent::Error(loc.tf(
"ui.err.bg_failed",
&[("label", kind_label(loc, kind)), ("reason", &reason)],
)));
}
self.note_slow_prefill(prefill);
}
fn give_back(&mut self, kind: BackgroundKind, window: Window) {
match window {
Window::Reflection { chat, upto, at } => {
let found = self.chats.iter_mut().find(|c| c.id == chat).map(|c| {
c.reflected_upto = upto;
c.reflected_at = at;
});
if found.is_some() {
self.mark_dirty(chat);
}
}
Window::Counter { chat, count } => {
let counts = match kind {
BackgroundKind::Consolidation => &mut self.consolidate_counts,
BackgroundKind::SelfConsolidation => &mut self.self_consolidate_counts,
BackgroundKind::Reflection | BackgroundKind::Compaction => return,
};
*counts.entry(chat).or_insert(0) += count;
}
}
}
pub(super) fn handle_stop_background_task(&self, kind: BackgroundKind) {
if let Some(token) = self.bg.get(&kind).and_then(|s| s.cancel.as_ref()) {
token.cancel();
}
}
pub(super) fn cancel_bg_all(&self) {
for slot in self.bg.values() {
if let Some(token) = &slot.cancel {
token.cancel();
}
}
}
fn any_bg_active(&self) -> bool {
self.bg.values().any(|s| s.cancel.is_some())
}
pub(super) async fn settle_silent_tasks(
&mut self,
done_rx: &mut UnboundedReceiver<BgDone>,
compact_rx: &mut UnboundedReceiver<CompactResult>,
cap: Option<Duration>,
) {
let deadline = cap.map(|cap| tokio::time::Instant::now() + cap);
while self.any_bg_active() {
let next = async {
tokio::select! {
landed = done_rx.recv() => landed.map(Landing::Task),
result = compact_rx.recv() => result.map(Landing::Roll),
}
};
let landed = match deadline {
Some(deadline) => tokio::time::timeout_at(deadline, next).await.ok(),
None => Some(next.await),
};
match landed {
Some(Some(Landing::Task(d))) => self.handle_bg_done(d.kind, d.outcome, d.prefill),
Some(Some(Landing::Roll(result))) => self.handle_compact_result(result),
Some(None) | None => break,
}
}
}
pub(super) fn refund_unlanded(&mut self) {
let refunds: Vec<(BackgroundKind, Window)> = self
.bg
.iter_mut()
.filter_map(|(kind, slot)| {
let refund = slot.refund.take()?;
(refund.acted.get() == Acting::Idle).then_some((*kind, refund.window))
})
.collect();
for (kind, window) in refunds {
self.give_back(kind, window);
}
}
#[cfg(test)]
pub(super) fn bg_failures(&self, kind: BackgroundKind) -> u32 {
self.bg.get(&kind).map_or(0, |s| s.failures)
}
}
pub(super) fn lane_label(kind: BackgroundKind) -> &'static str {
match kind {
BackgroundKind::Reflection => "reflection",
BackgroundKind::Consolidation => "consolidation",
BackgroundKind::SelfConsolidation => "self_consolidation",
BackgroundKind::Compaction => "compaction",
}
}
fn kind_label(loc: &'static Locale, kind: BackgroundKind) -> &'static str {
loc.t(match kind {
BackgroundKind::Reflection => "ui.err.bg_reflection",
BackgroundKind::Consolidation => "ui.err.bg_consolidation",
BackgroundKind::SelfConsolidation => "ui.err.bg_self_consolidation",
BackgroundKind::Compaction => "ui.err.bg_compaction",
})
}