use std::collections::{BTreeMap, BTreeSet};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use tokio::sync::{Notify, mpsc, watch};
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
use argonauts_core::engine::{EngineContext, EventSink, RepoContext, Services};
use argonauts_core::events::run::RunMode;
use argonauts_core::main_sessions::{MainSessionLease, MainSessions};
use argonauts_core::names::{ApprovalId, SessionKey};
use argonauts_core::policy::PolicyOutcome;
use argonauts_core::settings::Settings;
use argonauts_core::{RunId, TaskId};
use crate::approvals::Desk;
use crate::engine::job_fallback;
use crate::engine::journal::DeltaBuffer;
use crate::engine::live::LiveSessions;
use crate::engine::outside_writes::OutsideWrites;
use crate::engine::run_meter::RunMeter;
use crate::engine::tasks::Tasks;
use crate::error::EngineResult;
use crate::git::Git;
use crate::replay_terms::{self, ReplayTerms};
use crate::state::RunState;
use crate::state::ids::Counters;
use crate::status::Viewer;
use crate::status::files::StatusFiles;
use crate::status::folder::Folder;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Admission {
Open,
Paused,
Closed,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatusTarget {
pub path: PathBuf,
pub viewer: Viewer,
}
pub type PolicyResult = (ApprovalId, PolicyOutcome);
tokio::task_local! {
static GIT_ENV: Vec<(String, String)>;
}
pub async fn with_git_env<F: Future>(env: Vec<(String, String)>, future: F) -> F::Output {
GIT_ENV.scope(env, future).await
}
pub fn building_with_git_env<R>(env: Vec<(String, String)>, build: impl FnOnce() -> R) -> R {
GIT_ENV.sync_scope(env, build)
}
pub struct Shared {
pub run: RunId,
pub repo: RepoContext,
pub settings: Arc<Settings>,
pub settings_sha256: String,
pub sink: Arc<dyn EventSink>,
pub services: Services,
pub git: Git,
pub mode: RunMode,
pub state: Mutex<RunState>,
pub allocated: Mutex<Counters>,
pub live: LiveSessions,
pub start_turn: tokio::sync::Mutex<()>,
pub own_workflows: Mutex<BTreeSet<TaskId>>,
pub claude_codes_own: Mutex<BTreeMap<TaskId, crate::claude_codes::OwnSession>>,
pub said_ignored: std::sync::atomic::AtomicBool,
pub desk: Desk,
pub status: Mutex<Folder>,
pub status_files: Mutex<StatusFiles>,
pub status_targets: Mutex<BTreeMap<SessionKey, StatusTarget>>,
pub deltas: Mutex<DeltaBuffer>,
pub outside_writes: Mutex<OutsideWrites>,
pub admission: watch::Sender<Admission>,
interrupt: Mutex<CancellationToken>,
pub shutdown: CancellationToken,
pub tasks: Tasks,
pub halt: CancellationToken,
pub changed: Notify,
pub policy_outcomes: mpsc::UnboundedSender<PolicyResult>,
policy_inbox: Mutex<Option<mpsc::UnboundedReceiver<PolicyResult>>>,
pub mains: MainSessions,
pub leases: Mutex<BTreeMap<SessionKey, MainSessionLease>>,
pub pausing: Mutex<BTreeSet<SessionKey>>,
pub stopped: Mutex<BTreeMap<SessionKey, Option<TaskId>>>,
pub settling_pause: tokio::sync::Mutex<()>,
pub replay: Option<ReplayTerms>,
pub reusing: Mutex<()>,
pub run_meter: Mutex<RunMeter>,
pub told_left_out: Mutex<BTreeSet<String>>,
pub checking_goals: Mutex<BTreeSet<TaskId>>,
pub restarting: Mutex<BTreeSet<TaskId>>,
pub(crate) job_fallbacks: Mutex<job_fallback::Memory>,
pub(crate) starting_over: Mutex<BTreeSet<SessionKey>>,
pub(crate) asked_anyway: Mutex<BTreeMap<TaskId, u32>>,
pub(crate) start_checks: tokio::sync::Mutex<bool>,
}
impl Shared {
pub fn new(context: &EngineContext, state: RunState) -> EngineResult<Self> {
let mut status = Folder::default();
let mut run_meter = RunMeter::new(&context.settings.limits.spend);
for event in &context.history {
status.push(event);
run_meter.observe(event);
}
let (policy_outcomes, policy_inbox) = mpsc::unbounded_channel();
let repo_state = context
.repo
.run_dir
.parent()
.and_then(Path::parent)
.unwrap_or(&context.repo.run_dir);
let settings = context.settings.clone();
let git = GIT_ENV
.try_with(|env| Git::with_env(env.clone()))
.unwrap_or_default()
.leaving_out(
context.settings.merge.leave_out_builtin,
&context.settings.merge.leave_out,
)
.capping_new_files(context.settings.commit.max_new_file_mb);
let git = match context.repo.hidden_git() {
Some(hidden) => git.with_hidden(context.repo.root.clone(), hidden),
None => git,
};
Ok(Self {
run: context.run.clone(),
repo: context.repo.clone(),
settings_sha256: context.settings_sha256.clone(),
sink: context.sink.clone(),
services: context.services.clone(),
git,
mode: state.run.mode.unwrap_or(RunMode::Captain),
allocated: Mutex::new(state.ids.clone()),
state: Mutex::new(state),
live: LiveSessions::default(),
start_turn: tokio::sync::Mutex::new(()),
own_workflows: Mutex::new(BTreeSet::new()),
claude_codes_own: Mutex::new(BTreeMap::new()),
said_ignored: std::sync::atomic::AtomicBool::new(false),
desk: Desk::default(),
status: Mutex::new(status),
status_files: Mutex::new(StatusFiles::default()),
status_targets: Mutex::new(BTreeMap::new()),
deltas: Mutex::new(DeltaBuffer::new(
settings.run_host.journal_deltas_per_second,
)),
outside_writes: Mutex::new(OutsideWrites::default()),
admission: watch::channel(Admission::Open).0,
interrupt: Mutex::new(CancellationToken::new()),
shutdown: context.shutdown.clone(),
tasks: Tasks::default(),
halt: CancellationToken::new(),
changed: Notify::new(),
policy_outcomes,
policy_inbox: Mutex::new(Some(policy_inbox)),
mains: MainSessions::open(repo_state),
leases: Mutex::new(BTreeMap::new()),
pausing: Mutex::new(BTreeSet::new()),
stopped: Mutex::new(BTreeMap::new()),
settling_pause: tokio::sync::Mutex::new(()),
replay: replay_terms::read(&context.repo.run_dir)?,
reusing: Mutex::new(()),
run_meter: Mutex::new(run_meter),
told_left_out: Mutex::new(BTreeSet::new()),
checking_goals: Mutex::new(BTreeSet::new()),
restarting: Mutex::new(BTreeSet::new()),
job_fallbacks: Mutex::default(),
starting_over: Mutex::default(),
asked_anyway: Mutex::default(),
start_checks: tokio::sync::Mutex::new(false),
settings,
})
}
pub fn take_policy_inbox(&self) -> Option<mpsc::UnboundedReceiver<PolicyResult>> {
lock(&self.policy_inbox).take()
}
pub fn admission(&self) -> Admission {
*self.admission.borrow()
}
pub fn let_run_its_own_workflow(&self, task: &TaskId) {
lock(&self.own_workflows).insert(task.clone());
}
pub fn may_run_its_own_workflow(&self, task: &TaskId) -> bool {
lock(&self.own_workflows).contains(task)
}
pub fn work_as_claude_codes_own(&self, task: &TaskId, own: crate::claude_codes::OwnSession) {
lock(&self.own_workflows).insert(task.clone());
lock(&self.claude_codes_own).insert(task.clone(), own);
}
pub fn claude_codes_own(&self, task: &TaskId) -> Option<crate::claude_codes::OwnSession> {
lock(&self.claude_codes_own).get(task).cloned()
}
pub fn done_as_claude_codes_own(&self, task: &TaskId) {
lock(&self.claude_codes_own).remove(task);
}
pub fn interrupt(&self) -> Interrupt {
Interrupt(lock(&self.interrupt).clone())
}
pub fn renew_interrupt(&self) {
let mut interrupt = lock(&self.interrupt);
if interrupt.is_cancelled() {
*interrupt = CancellationToken::new();
}
}
}
#[derive(Debug, Clone)]
pub struct Interrupt(CancellationToken);
impl Interrupt {
pub fn cancelled(self) -> WaitForCancellationFutureOwned {
self.0.cancelled_owned()
}
pub fn cancel(&self) {
self.0.cancel();
}
}
pub fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}