1mod session_move;
4use crate::controller::move_session::{
5 MoveMutationGuard, MoveOutcome, MovePreparation, MoveSelection, MoveSessionRequest,
6};
7pub use mj_client::daemon::*;
8
9use std::collections::{BTreeMap, BTreeSet, VecDeque};
10use std::fs::{self, OpenOptions};
11use std::io::Write;
12use std::net::{IpAddr, Ipv4Addr, SocketAddr};
13use std::path::Path;
14use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, PoisonError};
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17
18use crate::database::StoreSchemaMismatch;
19use crate::recovery_gate::RecoveryObserver;
20use crate::targets::{
21 CancellableProcessExecutor, CommandExecutor, CommandOutput, CommandSpec, ProcessExecutor,
22 ProvisionStage, ProvisionStageGuard,
23};
24use agent_client_protocol::schema::v1::{ContentBlock, TextContent};
25use anyhow::{Context, Result, anyhow, bail, ensure};
26use mj_core::config::Config;
27use mj_core::refusal::Refusal;
28use mj_core::relay::RelayCommand;
29use mj_core::state::{RecoveryObservation, SessionRecord, SessionState};
30use mj_core::subagent::SubagentRecord;
31
32use crate::controller::{
33 BranchDisposition, Controller, ControllerStoreGuard, SessionLaunchOptions, SessionResumeOptions,
34};
35use crate::review_host::TurnReviewHost;
36use crate::session_manager::{
37 ManagedSessionView, RemoteSessionPublisher, RemoteSessionRequest, SessionManagerChannels,
38 SessionManagerControl, ViewError, new_command_id, spawn_remote_session_manager,
39 spawn_session_manager,
40};
41#[cfg(test)]
42use crate::session_manager::{RelaySessionTarget, RemoteSessionRequests, SessionManagerShutdown};
43use crate::worker_upgrade::{WorkerUpgradeObservation, WorkerUpgradeObserver};
44use mj_core::workspace::WorkspaceRecord;
45use tokio::net::{TcpListener, TcpStream};
46use tokio_util::sync::CancellationToken;
47
48use crate::pollers::{
49 dashboard_worker_targets, dashboard_worker_targets_excluding, interrupted_close_session_ids,
50 reserve_recovery_or_cancel, spawn_image_refresher, spawn_interrupted_close_recovery,
51 unowned_interrupted_lifecycles,
52};
53
54const SHUTDOWN_FORCE_EXIT_TIMEOUT: Duration = Duration::from_secs(10);
64
65const FORCE_DESTROY_PREEMPT_TIMEOUT: Duration = Duration::from_secs(8);
71
72#[derive(Clone, Default)]
74pub struct CreateSessionControl {
75 state: Arc<AtomicU8>,
76 pub cancelled: Arc<AtomicBool>,
77}
78
79impl CreateSessionControl {
80 pub fn request_cancel(&self) -> bool {
81 let accepted = self
82 .state
83 .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire)
84 .is_ok();
85 if accepted {
86 self.cancelled.store(true, Ordering::Release);
87 }
88 accepted
89 }
90
91 pub fn grant_commit(&self) -> bool {
92 self.state
93 .compare_exchange(0, 2, Ordering::AcqRel, Ordering::Acquire)
94 .is_ok()
95 }
96
97 fn is_cancellable(&self) -> bool {
98 self.state.load(Ordering::Acquire) == 0
99 }
100}
101
102#[derive(Debug, Clone)]
103struct Attachment {
104 pid: u32,
105}
106
107pub struct RuntimeState {
108 attachments: Mutex<BTreeMap<String, Attachment>>,
109 phone_status: Mutex<WebViewerStatus>,
110 pub web_viewer: crate::web_viewer::ViewerControl,
111 ever_attached: AtomicBool,
112 sessions: Mutex<BTreeMap<String, RuntimeSessionView>>,
113 revisions: RuntimeRevisions,
114 workspaces_tx: tokio::sync::watch::Sender<Vec<WorkspaceRecord>>,
115 session_manager: SessionManagerControl,
116 lifecycle: Mutex<BTreeMap<String, ActiveLifecycle>>,
117 startup_prompts: Mutex<BTreeMap<String, StartupQueue>>,
121 close_requested: Mutex<BTreeSet<String>>,
122 controller: Mutex<Controller>,
123 controller_loader: fn() -> Result<Controller>,
124 config_mutation: tokio::sync::Mutex<()>,
125 recovery_observer: RecoveryObserver,
126 worker_upgrade_observer: WorkerUpgradeObserver,
127 notices: Mutex<VecDeque<RuntimeNotice>>,
130 next_notice_id: AtomicU64,
131 review_config: Arc<Mutex<mj_core::config::ReviewConfig>>,
133 review_host: TurnReviewHost,
136 wiki: crate::sessionwiki::WikiIndexer,
138}
139
140#[derive(Clone)]
146struct RuntimeRevisions {
147 allocated: Arc<std::sync::atomic::AtomicU64>,
148 published: tokio::sync::watch::Sender<u64>,
149}
150
151impl RuntimeRevisions {
152 fn new(initial: u64) -> Self {
153 let (published, _) = tokio::sync::watch::channel(initial);
154 Self {
155 allocated: Arc::new(std::sync::atomic::AtomicU64::new(initial)),
156 published,
157 }
158 }
159
160 fn allocate(&self) -> u64 {
161 self.allocated.fetch_add(1, Ordering::AcqRel) + 1
162 }
163
164 fn publish(&self) -> u64 {
165 let revision = self.allocate();
166 self.publish_allocated(revision);
167 revision
168 }
169
170 fn publish_allocated(&self, revision: u64) {
171 self.published.send_if_modified(|visible| {
172 if revision > *visible {
173 *visible = revision;
174 true
175 } else {
176 false
177 }
178 });
179 }
180
181 fn notifier(&self) -> Arc<dyn Fn() + Send + Sync> {
182 let revisions = self.clone();
183 Arc::new(move || {
184 revisions.publish();
185 })
186 }
187
188 fn subscribe(&self) -> tokio::sync::watch::Receiver<u64> {
189 self.published.subscribe()
190 }
191
192 fn current(&self) -> u64 {
193 self.allocated.load(Ordering::Acquire)
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198enum LifecycleKind {
199 Create,
200 Close,
201 Resume,
202 Move,
203 ForceStop,
204 DestroyStopped,
205 ArchiveStopped,
209 ForceDestroy,
210 Cleanup,
211}
212
213fn lifecycle_owns_worker_target(kind: LifecycleKind, state: Option<SessionState>) -> bool {
218 match kind {
219 LifecycleKind::Close => state == Some(SessionState::Destroying),
220 LifecycleKind::Move => !matches!(
221 state,
222 Some(
223 SessionState::Running
224 | SessionState::Disconnected
225 | SessionState::Checkpointing
226 | SessionState::Closing
227 )
228 ),
229 _ => true,
230 }
231}
232
233fn lifecycle_cancellable(kind: LifecycleKind, state: Option<SessionState>) -> bool {
239 !(kind == LifecycleKind::Close && state == Some(SessionState::Destroying))
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244enum CloseRoute {
245 Graceful,
247 RecoverInterrupted,
249 SettleWithoutCheckpoint,
251 DeferredCleanup,
253 Done,
255}
256
257fn close_route(session: Option<&SessionRecord>) -> CloseRoute {
261 let Some(session) = session else {
262 return CloseRoute::Graceful;
263 };
264 if crate::pollers::is_interrupted_close(session) {
265 CloseRoute::RecoverInterrupted
266 } else if session.state == SessionState::Stopped {
267 if session.target.is_some() {
268 CloseRoute::DeferredCleanup
269 } else {
270 CloseRoute::Done
271 }
272 } else if crate::controller::has_nothing_to_checkpoint(session) {
273 CloseRoute::SettleWithoutCheckpoint
274 } else {
275 CloseRoute::Graceful
276 }
277}
278
279fn durable_session_state(controller: &Controller, session_id: &str) -> Option<SessionState> {
281 controller
282 .state
283 .sessions
284 .get(session_id)
285 .map(|session| session.state)
286}
287
288pub(crate) enum StartupStep {
294 InstallHandoff(Box<mj_core::archive::CanonicalSessionSnapshot>),
295 Prompt {
296 text: String,
297 inherited_draft: Option<String>,
298 },
299}
300
301struct StartupQueue {
307 pending: VecDeque<StartupStep>,
308 in_flight: bool,
309 cancel: CancellationToken,
310 task: Option<tokio::task::JoinHandle<()>>,
311}
312
313struct ActiveLifecycle {
314 operation_id: String,
315 create_control: Option<CreateSessionControl>,
316 kind: LifecycleKind,
317 cancelled: Arc<AtomicBool>,
318 started_at_epoch_seconds: u64,
319 active_stages: BTreeMap<ProvisionStage, (usize, u64)>,
320 resume_workspace_id: Option<String>,
323 resume_destination: Option<(String, String)>,
324 notice: Option<String>,
325 request_key: Option<String>,
326 _move_guard: Option<MoveMutationGuard>,
327 move_source_closed: bool,
328 result: LifecycleWatch,
329}
330
331impl ActiveLifecycle {
332 fn is_visible(&self) -> bool {
333 let result = self.result.borrow();
334 result.is_none()
335 || matches!(
336 result.as_ref(),
337 Some(Ok(DaemonLifecycleResult::DeferredCleanup))
338 )
339 }
340
341 fn request_cancel(&self) -> bool {
342 if let Some(control) = &self.create_control {
343 control.request_cancel()
344 } else {
345 !self.cancelled.swap(true, Ordering::AcqRel)
346 }
347 }
348
349 fn is_cancellable(&self) -> bool {
350 self.result.borrow().is_none()
351 && self.create_control.as_ref().map_or_else(
352 || !self.cancelled.load(Ordering::Acquire),
353 CreateSessionControl::is_cancellable,
354 )
355 }
356}
357
358#[derive(Debug, Clone)]
359enum DaemonLifecycleResult {
360 Done,
361 DeferredCleanup,
362 Move(MoveOutcome),
363}
364
365#[derive(Debug, Clone)]
372pub(crate) struct LifecycleFailure {
373 detail: String,
374 refusal: Option<Refusal>,
375}
376
377type LifecycleResult = std::result::Result<DaemonLifecycleResult, LifecycleFailure>;
380type LifecycleWatch = tokio::sync::watch::Receiver<Option<LifecycleResult>>;
381
382impl LifecycleFailure {
383 fn of(error: &anyhow::Error) -> Self {
384 Self {
385 detail: format!("{error:#}"),
386 refusal: Refusal::of(error),
387 }
388 }
389
390 fn internal(detail: impl Into<String>) -> Self {
393 Self {
394 detail: detail.into(),
395 refusal: None,
396 }
397 }
398
399 fn into_error(self) -> anyhow::Error {
401 match self.refusal {
402 Some(refusal) => anyhow::Error::new(refusal).context(self.detail),
403 None => anyhow::Error::msg(self.detail),
404 }
405 }
406}
407
408impl std::fmt::Display for LifecycleFailure {
409 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 formatter.write_str(&self.detail)
411 }
412}
413
414impl From<LifecycleKind> for RuntimeLifecycleKind {
415 fn from(kind: LifecycleKind) -> Self {
416 match kind {
417 LifecycleKind::Create => Self::Create,
418 LifecycleKind::Close => Self::Close,
419 LifecycleKind::Resume => Self::Resume,
420 LifecycleKind::Move => Self::Move,
421 LifecycleKind::ForceStop => Self::ForceStop,
422 LifecycleKind::DestroyStopped | LifecycleKind::ArchiveStopped => Self::DestroyStopped,
423 LifecycleKind::ForceDestroy => Self::ForceDestroy,
424 LifecycleKind::Cleanup => Self::Cleanup,
425 }
426 }
427}
428
429mod close;
430mod create;
431mod lifecycle;
432mod resume;
433mod snapshot;
434mod state;
435mod support;
436mod views;
437use support::*;
438mod process;
439pub use process::*;
440mod serve;
441use serve::*;
442mod actions;
443use actions::*;
444mod guards;
445pub(crate) use guards::*;
446
447#[cfg(test)]
448mod tests;