1use crate::review::RuntimeReviewView;
3use crate::session::{ManagedSessionView, ViewError};
4use anyhow::{Context, Result, bail, ensure};
5use mj_core::config::{Config, data_dir};
6use mj_core::credentials::CredentialSyncSignal;
7use mj_core::elicitation::ElicitationResponse;
8use mj_core::relay::{RelayCommand, RelayOperationalState};
9use mj_core::review::driver::Resolution;
10use mj_core::state::*;
11use mj_core::targets::{AdditionalMount, ProvisionStage};
12use mj_core::workspace::WorkspaceRecord;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::fs;
16use std::net::SocketAddr;
17use std::path::PathBuf;
18use std::time::{Duration, Instant};
19use tokio::io::{AsyncReadExt, AsyncWriteExt};
20use tokio::net::TcpStream;
21pub fn metadata_path() -> PathBuf {
22 data_dir().join("daemon.json")
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct DaemonMetadata {
28 pub protocol_version: u32,
29 pub pid: u32,
30 pub address: SocketAddr,
31 pub token: String,
32 pub started_at: String,
33 pub build_version: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct WorkspaceListing {
39 pub workspace: WorkspaceRecord,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct SessionPreview {
45 pub id: String,
46 pub title: String,
47 pub project: String,
48 pub harness: String,
49 pub state: String,
50 pub active: bool,
51 pub updated_at: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct WikiRow {
62 pub id: String,
63 pub tool: String,
64 pub project: String,
65 pub title: String,
66 pub started: Option<String>,
67 pub msgs: i64,
68 pub preview: Option<String>,
69 pub archived: bool,
71 pub native_id: Option<String>,
74 pub snippet: Option<String>,
76 pub hel_session_id: Option<String>,
78 #[serde(default)]
83 pub target: Option<String>,
84 #[serde(default)]
87 pub profile: Option<String>,
88 #[serde(default)]
92 pub harness: Option<String>,
93}
94
95#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum WikiIndexState {
99 Ready,
101 #[default]
104 Indexing,
105 VersionMismatch,
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct WikiStatus {
115 pub state: WikiIndexState,
116 pub topping_up: bool,
118}
119
120#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct WikiSearchPage {
124 pub rows: Vec<WikiRow>,
125 pub status: WikiStatus,
126}
127
128#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct WikiHitBlock {
133 pub role: String,
135 pub text: String,
138 pub hits: Vec<(usize, usize)>,
141 pub omitted_before: usize,
144 pub truncated: bool,
146}
147
148#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(deny_unknown_fields)]
151pub struct WikiHitTranscript {
152 pub blocks: Vec<WikiHitBlock>,
153 pub omitted_after: usize,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(deny_unknown_fields)]
160pub struct WikiRestoreRequest {
161 pub wiki_id: String,
163 pub workspace_id: String,
164 pub profile_id: String,
165 pub target_template_id: String,
166 #[serde(default)]
169 pub project_directory: Option<PathBuf>,
170 #[serde(default)]
171 pub additional_mounts: Vec<AdditionalMount>,
172 #[serde(default)]
173 pub resource_allocation: Option<SessionResourceAllocation>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(deny_unknown_fields)]
178pub struct WorkspaceSnapshot {
179 pub workspace: WorkspaceRecord,
180 pub sessions: Vec<SessionPreview>,
181 pub drafts: Vec<DraftPreview>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct RuntimeSessionView {
187 pub session_id: String,
188 pub projection_ordinal: u64,
189 pub projection_digest: String,
190 pub operational: Option<RelayOperationalState>,
191 pub latest_credential_sync_signal: Option<CredentialSyncSignal>,
192 pub connected: bool,
193 pub error: Option<ViewError>,
194}
195
196impl RuntimeSessionView {
197 pub fn from_managed(session_id: String, view: ManagedSessionView) -> Self {
198 let (projection_ordinal, projection_digest, operational, signal) =
199 view.snapshot
200 .map_or((0, String::new(), None, None), |snapshot| {
201 (
202 snapshot.materialized.applied_event_ordinal,
203 snapshot.materialized.applied_event_digest,
204 Some(snapshot.operational),
205 snapshot.latest_credential_sync_signal,
206 )
207 });
208 Self {
209 session_id,
210 projection_ordinal,
211 projection_digest,
212 operational,
213 latest_credential_sync_signal: signal,
214 connected: view.connected,
215 error: view.error,
216 }
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct RuntimeNotice {
228 pub id: u64,
229 pub session_id: String,
230 pub text: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct RuntimeSnapshot {
236 #[serde(default)]
237 pub workspace_names: BTreeMap<String, String>,
238 #[serde(default)]
239 pub moves: Vec<mj_core::state::MoveOperation>,
240 pub revision: u64,
241 pub config: Config,
242 pub records: Vec<SessionRecord>,
243 pub sessions: Vec<RuntimeSessionView>,
244 pub lifecycles: Vec<RuntimeLifecycleView>,
245 #[serde(default)]
247 pub reviews: Vec<RuntimeReviewView>,
248 #[serde(default)]
250 pub notices: Vec<RuntimeNotice>,
251 #[serde(default)]
255 pub subagents: Vec<mj_core::subagent::SubagentRecord>,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "snake_case")]
260pub enum RuntimeLifecycleKind {
261 Create,
262 Close,
263 Resume,
264 Move,
265 ForceStop,
266 DestroyStopped,
267 ForceDestroy,
268 Cleanup,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(deny_unknown_fields)]
273pub struct RuntimeLifecycleView {
274 pub operation_id: String,
275 pub cancellable: bool,
276 pub session_id: String,
277 pub kind: RuntimeLifecycleKind,
278 pub started_at_epoch_seconds: u64,
279 pub active_stages: Vec<(ProvisionStage, u64)>,
280 pub resume_destination: Option<(String, String)>,
281 pub notice: Option<String>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(deny_unknown_fields)]
286pub struct ResumeSessionRequest {
287 pub session_id: String,
288 pub workspace_id: String,
289 pub profile_id: String,
290 pub target_template_id: String,
291 pub additional_mounts: Option<Vec<AdditionalMount>>,
292 pub resource_allocation: Option<SessionResourceAllocation>,
293 pub discard_queue: bool,
294 pub repository_preflight: Option<ResumeRepositorySourceReceipt>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(deny_unknown_fields)]
299pub struct CreateSessionRequest {
300 #[serde(default)]
301 pub create_managed_worktree: Option<bool>,
302 #[serde(default)]
304 pub mjolnir_subagents: Option<bool>,
305 #[serde(default)]
306 pub initial_prompt: Option<String>,
307 pub workspace_id: String,
308 pub profile_id: String,
309 pub bundle_id: String,
310 pub project_directory: Option<PathBuf>,
311 pub target_template_id: String,
312 pub additional_mounts: Vec<AdditionalMount>,
313 pub resource_allocation: Option<SessionResourceAllocation>,
314 pub title: String,
315 pub session_title_override: Option<String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(deny_unknown_fields)]
320pub struct RegisteredSession {
321 pub session: SessionRecord,
322 pub remembered_container_size: Option<(String, HostContainerSize)>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326#[serde(deny_unknown_fields)]
327pub struct DraftPreview {
328 pub id: String,
329 pub session_id: Option<String>,
330 pub source: String,
331 pub owner_pid: Option<u32>,
332 pub saved_at: String,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(rename_all = "snake_case", tag = "action", content = "arguments")]
343pub enum DaemonAction {
344 Ping,
345 Status,
346 WebViewerAccess,
347 RecoverWebViewer(crate::web::WebViewerRecovery),
348 InspectWebListener,
349 ListWorkspaces,
350 CreateWorkspace {
351 name: String,
352 },
353 RenameWorkspace {
354 workspace_id: String,
355 name: String,
356 },
357 TouchWorkspace {
358 workspace_id: String,
359 },
360 DeleteWorkspace {
361 workspace_id: String,
362 },
363 Attach {
364 client_id: String,
365 pid: u32,
366 },
367 Detach {
368 client_id: String,
369 },
370 PersistReadReceipt {
371 client_id: String,
372 workspace_id: String,
373 session_id: String,
374 through: u64,
375 },
376 PersistDetachedSessionState {
377 client_id: String,
378 workspace_id: String,
379 session_id: String,
380 through: u64,
381 owner_pid: u32,
382 draft: mj_core::storage::DetachedSessionDraft,
383 },
384 SaveActiveReview {
385 session_id: String,
386 review: mj_core::storage::StoredReview,
387 },
388 ClearActiveReview {
389 session_id: String,
390 },
391 RememberReviewerSelection {
392 workspace_id: String,
393 selection: mj_core::second_opinion::ReviewerSelection,
394 },
395 SaveWorkspacePaneSizes {
396 workspace_id: String,
397 sizes: mj_core::workspace::PaneSizes,
398 },
399 SaveWorkspaceLayout {
400 workspace_id: String,
401 layout: mj_core::workspace::ConversationLayout,
402 },
403 PersistImportedSession {
404 session: Box<SessionRecord>,
405 },
406 SetSessionTitle {
407 session_id: String,
408 title: String,
409 },
410 SetSessionContainerSettings {
411 session_id: String,
412 cpus: Option<String>,
413 memory: Option<String>,
414 mounts: Vec<AdditionalMount>,
415 mount_history: Vec<PathBuf>,
416 },
417 SetSessionAcpTitle {
418 session_id: String,
419 title: Option<String>,
420 },
421 MarkSessionTargetMissing {
422 session_id: String,
423 detail: String,
424 updated_at: String,
425 },
426 CheckpointSession {
427 session_id: String,
428 },
429 WikiSearch {
432 query: String,
433 limit: usize,
434 },
435 WikiBrief {
437 wiki_id: String,
438 max_chars: usize,
439 },
440 WikiHits {
442 wiki_id: String,
443 query: String,
444 context_messages: usize,
445 per_message_chars: usize,
446 },
447 WikiRestore(WikiRestoreRequest),
449 ScanRecovery {
450 all_instances: bool,
451 },
452 AdoptRecovery {
453 session_id: String,
454 target_id: String,
455 profile: Option<String>,
456 bundle: Option<String>,
457 all_instances: bool,
458 },
459 DestroyRecovery {
460 session_id: String,
461 target_id: String,
462 confirmation: String,
463 all_instances: bool,
464 },
465 Snapshot {
466 workspace_id: String,
467 },
468 RuntimeSnapshot {
469 workspace_id: String,
470 after_revision: u64,
471 #[serde(default)]
472 all_workspaces: bool,
473 },
474 RenameProfile {
475 old_id: String,
476 new_id: String,
477 },
478 RenameTarget {
479 old_id: String,
480 new_id: String,
481 },
482 SubmitSessionCommand {
483 #[serde(default)]
484 inherited_draft: Option<String>,
485 session_id: String,
486 command_id: String,
487 command: RelayCommand,
488 },
489 QueueStartupPrompt {
494 session_id: String,
495 text: String,
496 #[serde(default)]
500 inherited_draft: Option<String>,
501 },
502 SyncSession {
503 session_id: String,
504 },
505 RespondElicitation {
506 session_id: String,
507 elicitation_id: String,
508 response: ElicitationResponse,
509 },
510 StopBackgroundTask {
511 session_id: String,
512 background_task_id: String,
513 },
514 ReviewerAction {
518 session_id: String,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
522 role: Option<String>,
523 action: crate::session::ReviewerAction,
524 },
525 StartTurnReview {
527 session_id: String,
528 },
529 ResolveTurnReview {
531 session_id: String,
532 resolution: Resolution,
533 },
534 CloseSession {
535 session_id: String,
536 },
537 StartCreateSession(CreateSessionRequest),
538 WaitCreateSession {
539 session_id: String,
540 },
541 ResumeSession(ResumeSessionRequest),
542 PrepareMoveSession(MoveSelection),
543 MoveSession(MoveSessionRequest),
544 ForceStopSession {
545 session_id: String,
546 },
547 DestroyStoppedSession {
548 session_id: String,
549 delete_branch: bool,
553 },
554 ForceDestroySession {
555 session_id: String,
556 delete_branch: bool,
558 },
559 ForceDeleteWorkspace {
560 workspace_id: String,
561 },
562 CancelLifecycle {
563 session_id: String,
564 },
565 RecoverDraft {
566 draft_id: String,
567 },
568 Stop,
569}
570
571#[derive(Debug, Serialize, Deserialize)]
572#[serde(deny_unknown_fields)]
573pub struct RequestEnvelope {
574 pub protocol_version: u32,
575 pub request_id: u64,
576 pub token: String,
577 pub action: DaemonAction,
578}
579
580#[derive(Debug, Serialize, Deserialize)]
581#[serde(deny_unknown_fields)]
582pub struct ResponseEnvelope {
583 pub protocol_version: u32,
584 pub request_id: u64,
585 pub result: std::result::Result<DaemonReply, String>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
589#[serde(rename_all = "snake_case", tag = "reply", content = "value")]
590pub enum DaemonReply {
591 Pong,
592 Status(DaemonStatus),
593 WebViewerAccess(crate::web::WebViewerAccess),
594 WebListeners(Vec<crate::web::WebListenerProcess>),
595 Workspaces(Vec<WorkspaceListing>),
596 Workspace(WorkspaceRecord),
597 Snapshot(WorkspaceSnapshot),
598 RuntimeSnapshot(Box<RuntimeSnapshot>),
599 RegisteredSession(Box<RegisteredSession>),
600 MovePreparation(Box<MovePreparation>),
601 MoveOutcome(MoveOutcome),
602 Ordinal(u64),
603 Text(String),
604 OptionalSessionState(Option<SessionState>),
605 Checkpoint(mj_core::state::CheckpointMetadata),
606 RecoveryScan(mj_core::state::RecoveryScan),
607 WikiRows(WikiSearchPage),
608 WikiHits(Option<WikiHitTranscript>),
609 Reviewer(Box<crate::session::ReviewerOutcome>),
610 Done,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
614#[serde(deny_unknown_fields)]
615pub struct DaemonStatus {
616 pub pid: u32,
617 pub started_at: String,
618 pub build_version: String,
619 pub attached_clients: usize,
620 pub phone_status: WebViewerStatus,
621}
622
623#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
624#[serde(rename_all = "snake_case", tag = "state")]
625pub enum WebViewerStatus {
626 Disabled,
627 Starting,
628 Ready {
629 viewer_url: String,
630 viewer_code: String,
631 qr_login_url: Option<String>,
632 fallback_reason: Option<String>,
633 },
634 Stopped,
635 Error {
636 message: String,
637 },
638}
639
640impl std::fmt::Debug for WebViewerStatus {
641 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 match self {
643 Self::Ready {
644 viewer_url,
645 viewer_code,
646 fallback_reason,
647 ..
648 } => formatter
649 .debug_struct("Ready")
650 .field("viewer_url", viewer_url)
651 .field("viewer_code", viewer_code)
652 .field("qr_login_url", &"[redacted]")
653 .field("fallback_reason", fallback_reason)
654 .finish(),
655 Self::Disabled => formatter.write_str("Disabled"),
656 Self::Starting => formatter.write_str("Starting"),
657 Self::Stopped => formatter.write_str("Stopped"),
658 Self::Error { message } => formatter.debug_tuple("Error").field(message).finish(),
659 }
660 }
661}
662
663impl std::fmt::Display for WebViewerStatus {
664 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 match self {
666 Self::Disabled => formatter.write_str("disabled"),
667 Self::Starting => formatter.write_str("starting"),
668 Self::Stopped => formatter.write_str("stopped unexpectedly"),
669 Self::Error { message } => write!(formatter, "error: {message}"),
670 Self::Ready {
671 viewer_url,
672 viewer_code,
673 fallback_reason,
674 ..
675 } => {
676 write!(formatter, "{viewer_url}; viewer code {viewer_code}")?;
677 if let Some(reason) = fallback_reason {
678 write!(
679 formatter,
680 "; local only because Tailscale HTTPS is unavailable: {reason}"
681 )?;
682 }
683 Ok(())
684 }
685 }
686 }
687}
688
689#[cfg(unix)]
704pub fn process_is_zombie(pid: u32) -> bool {
705 let pid = sysinfo::Pid::from_u32(pid);
706 let mut system = sysinfo::System::new();
707 system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
708 system
709 .process(pid)
710 .is_some_and(|process| process.status() == sysinfo::ProcessStatus::Zombie)
711}
712
713pub async fn wait_for_exit(pid: u32) -> Result<()> {
719 let deadline = Instant::now() + STOP_TIMEOUT;
720 while daemon_process_is_alive(pid) {
721 ensure!(Instant::now() < deadline, "process {pid} is still running");
722 tokio::time::sleep(RETRY_DELAY).await;
723 }
724 Ok(())
725}
726
727pub fn daemon_process_is_alive(pid: u32) -> bool {
733 #[cfg(unix)]
734 {
735 if pid == 0 {
736 return false;
737 }
738 let Ok(raw_pid) = libc::pid_t::try_from(pid) else {
739 return false;
740 };
741 let mut status = 0;
742 let waited = unsafe { libc::waitpid(raw_pid, &mut status, libc::WNOHANG) };
745 if waited == raw_pid {
746 return false;
747 }
748 if waited == 0 {
749 return true;
750 }
751 let wait_error = std::io::Error::last_os_error();
752 if wait_error.raw_os_error() != Some(libc::ECHILD) {
753 return true;
754 }
755
756 #[cfg(target_os = "macos")]
757 return owned_daemon_group_is_alive(raw_pid);
758
759 #[cfg(not(target_os = "macos"))]
760 process_is_alive(pid)
761 }
762 #[cfg(not(unix))]
763 process_is_alive(pid)
764}
765
766#[cfg(target_os = "macos")]
767pub fn owned_daemon_group_is_alive(pid: libc::pid_t) -> bool {
768 if unsafe { libc::kill(-pid, 0) } == 0 {
776 return true;
777 }
778 let error = std::io::Error::last_os_error();
779 !matches!(error.raw_os_error(), Some(libc::ESRCH) | Some(libc::EPERM))
780}
781
782pub fn process_is_alive(pid: u32) -> bool {
783 #[cfg(unix)]
784 {
785 if pid == 0 {
786 return false;
787 }
788 let Ok(raw_pid) = libc::pid_t::try_from(pid) else {
789 return false;
790 };
791 let result = unsafe { libc::kill(raw_pid, 0) };
794 let exists =
795 result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM);
796 exists && !process_is_zombie(pid)
797 }
798 #[cfg(not(unix))]
799 {
800 let _ = pid;
801 true
802 }
803}
804
805pub fn read_metadata() -> Result<DaemonMetadata> {
806 let metadata = read_metadata_any()?;
807 ensure!(
808 metadata.protocol_version == PROTOCOL_VERSION,
809 "daemon protocol {} is incompatible with client protocol {}",
810 metadata.protocol_version,
811 PROTOCOL_VERSION
812 );
813 Ok(metadata)
814}
815
816pub fn read_metadata_any() -> Result<DaemonMetadata> {
817 let path = metadata_path();
818 let body = fs::read(&path).with_context(|| format!("read {}", path.display()))?;
819 let metadata: DaemonMetadata =
820 serde_json::from_slice(&body).with_context(|| format!("parse {}", path.display()))?;
821 Ok(metadata)
822}
823
824pub async fn write_frame<T: Serialize>(stream: &mut TcpStream, value: &T) -> Result<()> {
825 let body = serde_json::to_vec(value)?;
826 ensure!(body.len() <= MAX_FRAME_BYTES, "daemon frame is too large");
827 stream.write_u32(body.len() as u32).await?;
828 stream.write_all(&body).await?;
829 stream.flush().await?;
830 Ok(())
831}
832
833pub async fn read_frame<T: for<'de> Deserialize<'de>>(stream: &mut TcpStream) -> Result<T> {
834 let length = stream.read_u32().await? as usize;
835 ensure!(
836 length <= MAX_FRAME_BYTES,
837 "daemon frame exceeds {MAX_FRAME_BYTES} bytes"
838 );
839 let mut body = vec![0_u8; length];
840 stream.read_exact(&mut body).await?;
841 serde_json::from_slice(&body).context("decode daemon frame")
842}
843
844pub struct DaemonClient {
845 metadata: DaemonMetadata,
846 stream: TcpStream,
847 next_request_id: u64,
848}
849
850impl DaemonClient {
851 pub async fn connect(metadata: DaemonMetadata) -> Result<Self> {
852 let stream =
853 tokio::time::timeout(Duration::from_secs(1), TcpStream::connect(metadata.address))
854 .await
855 .context("time out connecting to Mjolnir daemon")??;
856 Ok(Self {
857 metadata,
858 stream,
859 next_request_id: 1,
860 })
861 }
862
863 pub async fn request(&mut self, action: DaemonAction) -> Result<DaemonReply> {
867 let protocol_version = self.metadata.protocol_version;
868 let request_id = self.next_request_id;
869 self.next_request_id += 1;
870 write_frame(
871 &mut self.stream,
872 &RequestEnvelope {
873 protocol_version,
874 request_id,
875 token: self.metadata.token.clone(),
876 action,
877 },
878 )
879 .await?;
880 let response: ResponseEnvelope = read_frame(&mut self.stream).await?;
881 ensure!(
882 response.protocol_version == protocol_version,
883 "daemon changed protocol"
884 );
885 ensure!(
886 response.request_id == request_id,
887 "daemon crossed request IDs"
888 );
889 response.result.map_err(anyhow::Error::msg)
890 }
891
892 pub async fn status(&mut self) -> Result<DaemonStatus> {
893 match self.request(DaemonAction::Status).await? {
894 DaemonReply::Status(status) => Ok(status),
895 reply => bail!("unexpected daemon status reply {reply:?}"),
896 }
897 }
898
899 pub async fn web_access(&mut self) -> Result<crate::web::WebViewerAccess> {
900 match self.request(DaemonAction::WebViewerAccess).await? {
901 DaemonReply::WebViewerAccess(access) => Ok(access),
902 reply => bail!("unexpected web viewer reply {reply:?}"),
903 }
904 }
905
906 pub async fn recover_web_viewer(
907 &mut self,
908 action: crate::web::WebViewerRecovery,
909 ) -> Result<()> {
910 match self.request(DaemonAction::RecoverWebViewer(action)).await? {
911 DaemonReply::Done => Ok(()),
912 reply => bail!("unexpected web viewer recovery reply {reply:?}"),
913 }
914 }
915
916 pub async fn inspect_web_listener(&mut self) -> Result<Vec<crate::web::WebListenerProcess>> {
917 match self.request(DaemonAction::InspectWebListener).await? {
918 DaemonReply::WebListeners(processes) => Ok(processes),
919 reply => bail!("unexpected listener inspection reply {reply:?}"),
920 }
921 }
922
923 pub async fn list_workspaces(&mut self) -> Result<Vec<WorkspaceListing>> {
924 match self.request(DaemonAction::ListWorkspaces).await? {
925 DaemonReply::Workspaces(workspaces) => Ok(workspaces),
926 reply => bail!("unexpected daemon workspace reply {reply:?}"),
927 }
928 }
929
930 pub async fn rename_profile(&mut self, old_id: String, new_id: String) -> Result<()> {
931 match self
932 .request(DaemonAction::RenameProfile { old_id, new_id })
933 .await?
934 {
935 DaemonReply::Done => Ok(()),
936 reply => bail!("unexpected rename-profile reply {reply:?}"),
937 }
938 }
939
940 pub async fn rename_target(&mut self, old_id: String, new_id: String) -> Result<()> {
941 match self
942 .request(DaemonAction::RenameTarget { old_id, new_id })
943 .await?
944 {
945 DaemonReply::Done => Ok(()),
946 reply => bail!("unexpected rename-target reply {reply:?}"),
947 }
948 }
949
950 pub async fn create_workspace(&mut self, name: String) -> Result<WorkspaceRecord> {
951 match self.request(DaemonAction::CreateWorkspace { name }).await? {
952 DaemonReply::Workspace(workspace) => Ok(workspace),
953 reply => bail!("unexpected create-workspace reply {reply:?}"),
954 }
955 }
956
957 pub async fn rename_workspace(&mut self, workspace_id: String, name: String) -> Result<()> {
958 match self
959 .request(DaemonAction::RenameWorkspace { workspace_id, name })
960 .await?
961 {
962 DaemonReply::Done => Ok(()),
963 reply => bail!("unexpected rename-workspace reply {reply:?}"),
964 }
965 }
966
967 pub async fn touch_workspace(&mut self, workspace_id: String) -> Result<()> {
968 match self
969 .request(DaemonAction::TouchWorkspace { workspace_id })
970 .await?
971 {
972 DaemonReply::Done => Ok(()),
973 reply => bail!("unexpected touch-workspace reply {reply:?}"),
974 }
975 }
976
977 pub async fn delete_workspace(&mut self, workspace_id: String) -> Result<()> {
978 match self
979 .request(DaemonAction::DeleteWorkspace { workspace_id })
980 .await?
981 {
982 DaemonReply::Done => Ok(()),
983 reply => bail!("unexpected delete-workspace reply {reply:?}"),
984 }
985 }
986
987 pub async fn attach(&mut self, client_id: String, pid: u32) -> Result<()> {
988 match self
989 .request(DaemonAction::Attach { client_id, pid })
990 .await?
991 {
992 DaemonReply::Done => Ok(()),
993 reply => bail!("unexpected attach reply {reply:?}"),
994 }
995 }
996
997 pub async fn detach(&mut self, client_id: String) -> Result<()> {
998 match self.request(DaemonAction::Detach { client_id }).await? {
999 DaemonReply::Done => Ok(()),
1000 reply => bail!("unexpected detach reply {reply:?}"),
1001 }
1002 }
1003
1004 pub async fn persist_read_receipt(
1005 &mut self,
1006 client_id: String,
1007 workspace_id: String,
1008 session_id: String,
1009 through: u64,
1010 ) -> Result<u64> {
1011 match self
1012 .request(DaemonAction::PersistReadReceipt {
1013 client_id,
1014 workspace_id,
1015 session_id,
1016 through,
1017 })
1018 .await?
1019 {
1020 DaemonReply::Ordinal(ordinal) => Ok(ordinal),
1021 reply => bail!("unexpected read-receipt reply {reply:?}"),
1022 }
1023 }
1024
1025 pub async fn persist_detached_session_state(
1026 &mut self,
1027 client_id: String,
1028 workspace_id: String,
1029 session_id: String,
1030 through: u64,
1031 owner_pid: u32,
1032 draft: mj_core::storage::DetachedSessionDraft,
1033 ) -> Result<()> {
1034 match self
1035 .request(DaemonAction::PersistDetachedSessionState {
1036 client_id,
1037 workspace_id,
1038 session_id,
1039 through,
1040 owner_pid,
1041 draft,
1042 })
1043 .await?
1044 {
1045 DaemonReply::Done => Ok(()),
1046 reply => bail!("unexpected detached-session-state reply {reply:?}"),
1047 }
1048 }
1049
1050 pub async fn save_active_review(
1051 &mut self,
1052 session_id: String,
1053 review: mj_core::storage::StoredReview,
1054 ) -> Result<()> {
1055 match self
1056 .request(DaemonAction::SaveActiveReview { session_id, review })
1057 .await?
1058 {
1059 DaemonReply::Done => Ok(()),
1060 reply => bail!("unexpected save-review reply {reply:?}"),
1061 }
1062 }
1063
1064 pub async fn clear_active_review(&mut self, session_id: String) -> Result<()> {
1065 match self
1066 .request(DaemonAction::ClearActiveReview { session_id })
1067 .await?
1068 {
1069 DaemonReply::Done => Ok(()),
1070 reply => bail!("unexpected clear-review reply {reply:?}"),
1071 }
1072 }
1073
1074 pub async fn remember_reviewer_selection(
1075 &mut self,
1076 workspace_id: String,
1077 selection: mj_core::second_opinion::ReviewerSelection,
1078 ) -> Result<()> {
1079 match self
1080 .request(DaemonAction::RememberReviewerSelection {
1081 workspace_id,
1082 selection,
1083 })
1084 .await?
1085 {
1086 DaemonReply::Done => Ok(()),
1087 reply => bail!("unexpected reviewer-selection reply {reply:?}"),
1088 }
1089 }
1090
1091 pub async fn save_workspace_pane_sizes(
1092 &mut self,
1093 workspace_id: String,
1094 sizes: mj_core::workspace::PaneSizes,
1095 ) -> Result<()> {
1096 match self
1097 .request(DaemonAction::SaveWorkspacePaneSizes {
1098 workspace_id,
1099 sizes,
1100 })
1101 .await?
1102 {
1103 DaemonReply::Done => Ok(()),
1104 reply => bail!("unexpected pane-size save reply {reply:?}"),
1105 }
1106 }
1107
1108 pub async fn save_workspace_layout(
1109 &mut self,
1110 workspace_id: String,
1111 layout: mj_core::workspace::ConversationLayout,
1112 ) -> Result<()> {
1113 match self
1114 .request(DaemonAction::SaveWorkspaceLayout {
1115 workspace_id,
1116 layout,
1117 })
1118 .await?
1119 {
1120 DaemonReply::Done => Ok(()),
1121 reply => bail!("unexpected layout save reply {reply:?}"),
1122 }
1123 }
1124
1125 pub async fn persist_imported_session(&mut self, session: SessionRecord) -> Result<()> {
1126 match self
1127 .request(DaemonAction::PersistImportedSession {
1128 session: Box::new(session),
1129 })
1130 .await?
1131 {
1132 DaemonReply::Done => Ok(()),
1133 reply => bail!("unexpected imported-session reply {reply:?}"),
1134 }
1135 }
1136
1137 pub async fn set_session_title(&mut self, session_id: String, title: String) -> Result<String> {
1138 match self
1139 .request(DaemonAction::SetSessionTitle { session_id, title })
1140 .await?
1141 {
1142 DaemonReply::Text(title) => Ok(title),
1143 reply => bail!("unexpected session-title reply {reply:?}"),
1144 }
1145 }
1146
1147 pub async fn set_session_container_settings(
1148 &mut self,
1149 session_id: String,
1150 cpus: Option<String>,
1151 memory: Option<String>,
1152 mounts: Vec<AdditionalMount>,
1153 mount_history: Vec<PathBuf>,
1154 ) -> Result<()> {
1155 match self
1156 .request(DaemonAction::SetSessionContainerSettings {
1157 session_id,
1158 cpus,
1159 memory,
1160 mounts,
1161 mount_history,
1162 })
1163 .await?
1164 {
1165 DaemonReply::Done => Ok(()),
1166 reply => bail!("unexpected container-settings reply {reply:?}"),
1167 }
1168 }
1169
1170 pub async fn set_session_acp_title(
1171 &mut self,
1172 session_id: String,
1173 title: Option<String>,
1174 ) -> Result<()> {
1175 match self
1176 .request(DaemonAction::SetSessionAcpTitle { session_id, title })
1177 .await?
1178 {
1179 DaemonReply::Done => Ok(()),
1180 reply => bail!("unexpected ACP-title reply {reply:?}"),
1181 }
1182 }
1183
1184 pub async fn mark_session_target_missing(
1185 &mut self,
1186 session_id: String,
1187 detail: String,
1188 updated_at: String,
1189 ) -> Result<Option<SessionState>> {
1190 match self
1191 .request(DaemonAction::MarkSessionTargetMissing {
1192 session_id,
1193 detail,
1194 updated_at,
1195 })
1196 .await?
1197 {
1198 DaemonReply::OptionalSessionState(state) => Ok(state),
1199 reply => bail!("unexpected target-missing reply {reply:?}"),
1200 }
1201 }
1202
1203 pub async fn checkpoint_session(
1204 &mut self,
1205 session_id: String,
1206 ) -> Result<mj_core::state::CheckpointMetadata> {
1207 match self
1208 .request(DaemonAction::CheckpointSession { session_id })
1209 .await?
1210 {
1211 DaemonReply::Checkpoint(checkpoint) => Ok(checkpoint),
1212 reply => bail!("unexpected checkpoint reply {reply:?}"),
1213 }
1214 }
1215
1216 pub async fn wiki_search(&mut self, query: String, limit: usize) -> Result<WikiSearchPage> {
1221 match self
1222 .request(DaemonAction::WikiSearch { query, limit })
1223 .await?
1224 {
1225 DaemonReply::WikiRows(page) => Ok(page),
1226 reply => bail!("unexpected SessionWiki search reply {reply:?}"),
1227 }
1228 }
1229
1230 pub async fn wiki_brief(&mut self, wiki_id: String, max_chars: usize) -> Result<String> {
1232 match self
1233 .request(DaemonAction::WikiBrief { wiki_id, max_chars })
1234 .await?
1235 {
1236 DaemonReply::Text(markdown) => Ok(markdown),
1237 reply => bail!("unexpected SessionWiki brief reply {reply:?}"),
1238 }
1239 }
1240
1241 pub async fn wiki_hits(
1246 &mut self,
1247 wiki_id: String,
1248 query: String,
1249 context_messages: usize,
1250 per_message_chars: usize,
1251 ) -> Result<Option<WikiHitTranscript>> {
1252 match self
1253 .request(DaemonAction::WikiHits {
1254 wiki_id,
1255 query,
1256 context_messages,
1257 per_message_chars,
1258 })
1259 .await?
1260 {
1261 DaemonReply::WikiHits(transcript) => Ok(transcript),
1262 reply => bail!("unexpected SessionWiki hits reply {reply:?}"),
1263 }
1264 }
1265
1266 pub async fn wiki_restore(&mut self, request: WikiRestoreRequest) -> Result<RegisteredSession> {
1270 match self.request(DaemonAction::WikiRestore(request)).await? {
1271 DaemonReply::RegisteredSession(registered) => Ok(*registered),
1272 reply => bail!("unexpected SessionWiki restore reply {reply:?}"),
1273 }
1274 }
1275
1276 pub async fn scan_recovery(
1277 &mut self,
1278 all_instances: bool,
1279 ) -> Result<mj_core::state::RecoveryScan> {
1280 match self
1281 .request(DaemonAction::ScanRecovery { all_instances })
1282 .await?
1283 {
1284 DaemonReply::RecoveryScan(scan) => Ok(scan),
1285 reply => bail!("unexpected recovery-scan reply {reply:?}"),
1286 }
1287 }
1288
1289 pub async fn adopt_recovery(
1290 &mut self,
1291 session_id: String,
1292 target_id: String,
1293 profile: Option<String>,
1294 bundle: Option<String>,
1295 all_instances: bool,
1296 ) -> Result<()> {
1297 match self
1298 .request(DaemonAction::AdoptRecovery {
1299 session_id,
1300 target_id,
1301 profile,
1302 bundle,
1303 all_instances,
1304 })
1305 .await?
1306 {
1307 DaemonReply::Done => Ok(()),
1308 reply => bail!("unexpected recovery-adopt reply {reply:?}"),
1309 }
1310 }
1311
1312 pub async fn destroy_recovery(
1313 &mut self,
1314 session_id: String,
1315 target_id: String,
1316 confirmation: String,
1317 all_instances: bool,
1318 ) -> Result<()> {
1319 match self
1320 .request(DaemonAction::DestroyRecovery {
1321 session_id,
1322 target_id,
1323 confirmation,
1324 all_instances,
1325 })
1326 .await?
1327 {
1328 DaemonReply::Done => Ok(()),
1329 reply => bail!("unexpected recovery-destroy reply {reply:?}"),
1330 }
1331 }
1332
1333 pub async fn snapshot(&mut self, workspace_id: String) -> Result<WorkspaceSnapshot> {
1334 match self
1335 .request(DaemonAction::Snapshot { workspace_id })
1336 .await?
1337 {
1338 DaemonReply::Snapshot(snapshot) => Ok(snapshot),
1339 reply => bail!("unexpected snapshot reply {reply:?}"),
1340 }
1341 }
1342
1343 pub async fn runtime_snapshot(
1344 &mut self,
1345 workspace_id: String,
1346 after_revision: u64,
1347 all_workspaces: bool,
1348 ) -> Result<RuntimeSnapshot> {
1349 match self
1350 .request(DaemonAction::RuntimeSnapshot {
1351 workspace_id,
1352 after_revision,
1353 all_workspaces,
1354 })
1355 .await?
1356 {
1357 DaemonReply::RuntimeSnapshot(snapshot) => Ok(*snapshot),
1358 reply => bail!("unexpected runtime snapshot reply {reply:?}"),
1359 }
1360 }
1361
1362 pub async fn submit_session_command(
1363 &mut self,
1364 session_id: String,
1365 command_id: String,
1366 command: RelayCommand,
1367 inherited_draft: Option<String>,
1368 ) -> Result<u64> {
1369 match self
1370 .request(DaemonAction::SubmitSessionCommand {
1371 inherited_draft,
1372 session_id,
1373 command_id,
1374 command,
1375 })
1376 .await?
1377 {
1378 DaemonReply::Ordinal(ordinal) => Ok(ordinal),
1379 reply => bail!("unexpected session command reply {reply:?}"),
1380 }
1381 }
1382
1383 pub async fn queue_startup_prompt(
1387 &mut self,
1388 session_id: String,
1389 text: String,
1390 inherited_draft: Option<String>,
1391 ) -> Result<()> {
1392 match self
1393 .request(DaemonAction::QueueStartupPrompt {
1394 session_id,
1395 text,
1396 inherited_draft,
1397 })
1398 .await?
1399 {
1400 DaemonReply::Done => Ok(()),
1401 reply => bail!("unexpected startup prompt reply {reply:?}"),
1402 }
1403 }
1404
1405 pub async fn start_turn_review(&mut self, session_id: String) -> Result<()> {
1411 match self
1412 .request(DaemonAction::StartTurnReview { session_id })
1413 .await?
1414 {
1415 DaemonReply::Done => Ok(()),
1416 reply => bail!("unexpected turn-review reply {reply:?}"),
1417 }
1418 }
1419
1420 pub async fn resolve_turn_review(
1421 &mut self,
1422 session_id: String,
1423 resolution: Resolution,
1424 ) -> Result<()> {
1425 match self
1426 .request(DaemonAction::ResolveTurnReview {
1427 session_id,
1428 resolution,
1429 })
1430 .await?
1431 {
1432 DaemonReply::Done => Ok(()),
1433 reply => bail!("unexpected turn-review resolution reply {reply:?}"),
1434 }
1435 }
1436
1437 pub async fn reviewer_action(
1438 &mut self,
1439 session_id: String,
1440 role: Option<String>,
1441 action: crate::session::ReviewerAction,
1442 ) -> Result<crate::session::ReviewerOutcome> {
1443 match self
1444 .request(DaemonAction::ReviewerAction {
1445 session_id,
1446 role,
1447 action,
1448 })
1449 .await?
1450 {
1451 DaemonReply::Reviewer(outcome) => Ok(*outcome),
1452 reply => bail!("unexpected reviewer reply {reply:?}"),
1453 }
1454 }
1455
1456 pub async fn sync_session(&mut self, session_id: String) -> Result<()> {
1457 match self
1458 .request(DaemonAction::SyncSession { session_id })
1459 .await?
1460 {
1461 DaemonReply::Done => Ok(()),
1462 reply => bail!("unexpected session sync reply {reply:?}"),
1463 }
1464 }
1465
1466 pub async fn respond_elicitation(
1467 &mut self,
1468 session_id: String,
1469 elicitation_id: String,
1470 response: ElicitationResponse,
1471 ) -> Result<()> {
1472 match self
1473 .request(DaemonAction::RespondElicitation {
1474 session_id,
1475 elicitation_id,
1476 response,
1477 })
1478 .await?
1479 {
1480 DaemonReply::Done => Ok(()),
1481 reply => bail!("unexpected elicitation reply {reply:?}"),
1482 }
1483 }
1484
1485 pub async fn stop_background_task(
1486 &mut self,
1487 session_id: String,
1488 background_task_id: String,
1489 ) -> Result<()> {
1490 match self
1491 .request(DaemonAction::StopBackgroundTask {
1492 session_id,
1493 background_task_id,
1494 })
1495 .await?
1496 {
1497 DaemonReply::Done => Ok(()),
1498 reply => bail!("unexpected background task stop reply {reply:?}"),
1499 }
1500 }
1501
1502 pub async fn close_session(&mut self, session_id: String) -> Result<()> {
1503 match self
1504 .request(DaemonAction::CloseSession { session_id })
1505 .await?
1506 {
1507 DaemonReply::Done => Ok(()),
1508 reply => bail!("unexpected close-session reply {reply:?}"),
1509 }
1510 }
1511
1512 pub async fn start_create_session(
1513 &mut self,
1514 request: CreateSessionRequest,
1515 ) -> Result<RegisteredSession> {
1516 match self
1517 .request(DaemonAction::StartCreateSession(request))
1518 .await?
1519 {
1520 DaemonReply::RegisteredSession(registered) => Ok(*registered),
1521 reply => bail!("unexpected start-create reply {reply:?}"),
1522 }
1523 }
1524
1525 pub async fn wait_create_session(&mut self, session_id: String) -> Result<()> {
1526 match self
1527 .request(DaemonAction::WaitCreateSession { session_id })
1528 .await?
1529 {
1530 DaemonReply::Done => Ok(()),
1531 reply => bail!("unexpected wait-create reply {reply:?}"),
1532 }
1533 }
1534
1535 pub async fn resume_session(&mut self, request: ResumeSessionRequest) -> Result<()> {
1536 match self.request(DaemonAction::ResumeSession(request)).await? {
1537 DaemonReply::Done => Ok(()),
1538 reply => bail!("unexpected resume-session reply {reply:?}"),
1539 }
1540 }
1541
1542 pub async fn force_stop_session(&mut self, session_id: String) -> Result<()> {
1543 match self
1544 .request(DaemonAction::ForceStopSession { session_id })
1545 .await?
1546 {
1547 DaemonReply::Done => Ok(()),
1548 reply => bail!("unexpected force-stop reply {reply:?}"),
1549 }
1550 }
1551
1552 pub async fn destroy_stopped_session(
1553 &mut self,
1554 session_id: String,
1555 delete_branch: bool,
1556 ) -> Result<()> {
1557 match self
1558 .request(DaemonAction::DestroyStoppedSession {
1559 session_id,
1560 delete_branch,
1561 })
1562 .await?
1563 {
1564 DaemonReply::Done => Ok(()),
1565 reply => bail!("unexpected destroy-stopped reply {reply:?}"),
1566 }
1567 }
1568
1569 pub async fn force_destroy_session(
1570 &mut self,
1571 session_id: String,
1572 delete_branch: bool,
1573 ) -> Result<()> {
1574 match self
1575 .request(DaemonAction::ForceDestroySession {
1576 session_id,
1577 delete_branch,
1578 })
1579 .await?
1580 {
1581 DaemonReply::Done => Ok(()),
1582 reply => bail!("unexpected force-destroy reply {reply:?}"),
1583 }
1584 }
1585
1586 pub async fn force_delete_workspace(&mut self, workspace_id: String) -> Result<()> {
1587 match self
1588 .request(DaemonAction::ForceDeleteWorkspace { workspace_id })
1589 .await?
1590 {
1591 DaemonReply::Done => Ok(()),
1592 reply => bail!("unexpected force-delete-workspace reply {reply:?}"),
1593 }
1594 }
1595
1596 pub async fn cancel_lifecycle(&mut self, session_id: String) -> Result<()> {
1597 match self
1598 .request(DaemonAction::CancelLifecycle { session_id })
1599 .await?
1600 {
1601 DaemonReply::Done => Ok(()),
1602 reply => bail!("unexpected cancel-lifecycle reply {reply:?}"),
1603 }
1604 }
1605
1606 pub async fn recover_draft(&mut self, draft_id: String) -> Result<()> {
1607 match self
1608 .request(DaemonAction::RecoverDraft { draft_id })
1609 .await?
1610 {
1611 DaemonReply::Done => Ok(()),
1612 reply => bail!("unexpected recover-draft reply {reply:?}"),
1613 }
1614 }
1615
1616 pub async fn stop(&mut self) -> Result<()> {
1617 match self.request(DaemonAction::Stop).await? {
1618 DaemonReply::Done => Ok(()),
1619 reply => bail!("unexpected stop reply {reply:?}"),
1620 }
1621 }
1622}
1623
1624pub async fn connect_existing() -> Result<DaemonClient> {
1625 let metadata = tokio::task::spawn_blocking(read_metadata)
1626 .await
1627 .context("read daemon metadata task failed")??;
1628 DaemonClient::connect(metadata).await
1629}
1630
1631pub struct ManagementClient {
1635 inner: DaemonClient,
1636}
1637
1638impl ManagementClient {
1639 pub fn new(inner: DaemonClient) -> Self {
1640 Self { inner }
1641 }
1642 pub fn protocol_version(&self) -> u32 {
1643 self.inner.metadata.protocol_version
1644 }
1645
1646 pub async fn status(&mut self) -> Result<DaemonStatus> {
1647 self.inner.status().await
1648 }
1649
1650 pub async fn stop(&mut self) -> Result<()> {
1651 tokio::time::timeout(STOP_TIMEOUT, self.inner.stop())
1652 .await
1653 .context("Mjolnir daemon did not acknowledge the stop before the deadline")?
1654 }
1655
1656 pub async fn stop_and_wait(mut self) -> Result<()> {
1658 let pid = self.inner.metadata.pid;
1659 self.stop().await?;
1660 wait_for_exit(pid).await.with_context(|| {
1661 format!(
1662 "Mjolnir daemon {pid} accepted the stop but was still running after {}s",
1663 STOP_TIMEOUT.as_secs()
1664 )
1665 })
1666 }
1667}
1668
1669pub async fn connect_management() -> Result<ManagementClient> {
1670 Ok(ManagementClient {
1671 inner: DaemonClient::connect(read_metadata_any()?).await?,
1672 })
1673}
1674
1675pub fn ensure_supported_daemon_protocol(version: u32) -> Result<()> {
1676 ensure!(
1677 version <= PROTOCOL_VERSION,
1678 "the daemon uses a newer protocol ({version}) than this client ({PROTOCOL_VERSION}); restart this client with the updated mj binary"
1679 );
1680 Ok(())
1681}
1682pub const PROTOCOL_VERSION: u32 = 26;
1683pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
1684pub const STOP_TIMEOUT: Duration = Duration::from_secs(30);
1694pub const RETRY_DELAY: Duration = Duration::from_millis(40);
1695impl DaemonClient {
1696 pub async fn prepare_move_session(
1697 &mut self,
1698 selection: MoveSelection,
1699 ) -> Result<MovePreparation> {
1700 match self
1701 .request(DaemonAction::PrepareMoveSession(selection))
1702 .await?
1703 {
1704 DaemonReply::MovePreparation(preparation) => Ok(*preparation),
1705 _ => bail!("daemon returned an unexpected move preparation reply"),
1706 }
1707 }
1708
1709 pub async fn move_session(&mut self, request: MoveSessionRequest) -> Result<MoveOutcome> {
1710 match self.request(DaemonAction::MoveSession(request)).await? {
1711 DaemonReply::MoveOutcome(outcome) => Ok(outcome),
1712 _ => bail!("daemon returned an unexpected move reply"),
1713 }
1714 }
1715}