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