Skip to main content

mj_controller/session_manager/
remote.rs

1use super::*;
2
3pub(super) fn reconcile_action(
4    actor: Option<&RelaySessionTarget>,
5    desired: Option<&RelaySessionTarget>,
6) -> ReconcileAction {
7    match (actor, desired) {
8        (None, None) => ReconcileAction::Idle,
9        (None, Some(_)) => ReconcileAction::Spawn,
10        (Some(actor), Some(desired)) if actor == desired => ReconcileAction::Keep,
11        (Some(_), Some(_) | None) => ReconcileAction::Retire,
12    }
13}
14
15pub(super) fn target_map(targets: &[RelaySessionTarget]) -> BTreeMap<String, RelaySessionTarget> {
16    targets
17        .iter()
18        .cloned()
19        .map(|target| (target.session_id.clone(), target))
20        .collect()
21}
22
23pub(super) fn remove_actor_task(
24    actors: &mut BTreeMap<String, ActorRegistration>,
25    task_id: tokio::task::Id,
26) -> Option<String> {
27    let session_id = actors.iter().find_map(|(session_id, actor)| {
28        (actor.abort.id() == task_id).then(|| session_id.clone())
29    })?;
30    actors.remove(&session_id);
31    Some(session_id)
32}
33
34pub(super) fn reconcile_actors(
35    targets: &BTreeMap<String, RelaySessionTarget>,
36    actors: &mut BTreeMap<String, ActorRegistration>,
37    tasks: &mut tokio::task::JoinSet<String>,
38    updates: &CoalescedUpdateSender,
39) {
40    // A completed or cancelled task closes its command receiver before the
41    // JoinSet completion necessarily wins the manager's select. Do not let
42    // that dead registration suppress the replacement this reconciliation is
43    // responsible for starting. Task-ID-aware completion cleanup below keeps
44    // the old completion from removing the replacement later.
45    actors.retain(|session_id, actor| {
46        let live = !actor.commands.is_closed();
47        if !live {
48            tracing::warn!(session_id, "replacing stopped session relay actor");
49        }
50        live
51    });
52
53    for (session_id, actor) in actors.iter() {
54        let retiring = matches!(
55            reconcile_action(Some(&actor.target), targets.get(session_id)),
56            ReconcileAction::Retire
57        );
58        actor.retirement.send_replace(retiring);
59    }
60
61    for (session_id, target) in targets {
62        if !matches!(
63            reconcile_action(
64                actors.get(session_id).map(|actor| &actor.target),
65                Some(target)
66            ),
67            ReconcileAction::Spawn
68        ) {
69            continue;
70        }
71        let (actor_tx, actor_rx) = mpsc::channel(32);
72        let (release_tx, release_rx) = mpsc::unbounded_channel();
73        let (retirement_tx, retirement_rx) = watch::channel(false);
74        let (view_tx, view_rx) = watch::channel(ManagedSessionView::default());
75        let actor_updates = updates.clone();
76        let task_target = target.clone();
77        let task_id = session_id.clone();
78        let abort = tasks.spawn(async move {
79            run_session_actor(
80                task_target,
81                actor_rx,
82                release_rx,
83                retirement_rx,
84                view_tx,
85                actor_updates,
86            )
87            .await;
88            task_id
89        });
90        actors.insert(
91            session_id.clone(),
92            ActorRegistration {
93                target: target.clone(),
94                commands: actor_tx,
95                releases: release_tx,
96                retirement: retirement_tx,
97                view: view_rx,
98                abort,
99            },
100        );
101    }
102}
103
104pub(super) async fn run_remote_session_actor(
105    session_id: String,
106    mut commands: mpsc::Receiver<ActorCommand>,
107    requests: mpsc::Sender<RemoteSessionRequest>,
108) {
109    while let Some(command) = commands.recv().await {
110        let request = match command {
111            ActorCommand::Submit {
112                command_id,
113                command,
114                admission,
115                reply,
116            } => RemoteSessionRequest::Submit {
117                session_id: session_id.clone(),
118                command_id,
119                command,
120                admission,
121                reply,
122            },
123            ActorCommand::Sync { reply } => RemoteSessionRequest::Sync {
124                session_id: session_id.clone(),
125                reply,
126            },
127            ActorCommand::RespondElicitation {
128                elicitation_id,
129                response,
130                reply,
131            } => RemoteSessionRequest::RespondElicitation {
132                session_id: session_id.clone(),
133                elicitation_id,
134                response,
135                reply,
136            },
137            ActorCommand::StopBackgroundTask {
138                background_task_id,
139                reply,
140            } => RemoteSessionRequest::StopBackgroundTask {
141                session_id: session_id.clone(),
142                background_task_id,
143                reply,
144            },
145            ActorCommand::Reviewer {
146                role,
147                action,
148                reply,
149            } => RemoteSessionRequest::Reviewer {
150                session_id: session_id.clone(),
151                role,
152                action,
153                reply,
154            },
155            ActorCommand::InstallPromptContext { reply, .. } => {
156                // Only the daemon that owns the relay can install context, and
157                // only a session it started is ever restored into.
158                let _ = reply.send(Err(
159                    "prompt context can be installed only inside the controller daemon".into(),
160                ));
161                continue;
162            }
163            ActorCommand::Lease { reply } => {
164                let _ = reply.send(Err(anyhow::anyhow!(
165                    "relay connection leases are available only inside the controller daemon"
166                )));
167                continue;
168            }
169        };
170        if let Err(error) = requests.send(request).await {
171            match error.0 {
172                RemoteSessionRequest::Submit { reply, .. } => {
173                    let _ = reply.send(Err("controller daemon request bridge stopped".into()));
174                }
175                RemoteSessionRequest::Sync { reply, .. }
176                | RemoteSessionRequest::RespondElicitation { reply, .. }
177                | RemoteSessionRequest::StopBackgroundTask { reply, .. } => {
178                    let _ = reply.send(Err("controller daemon request bridge stopped".into()));
179                }
180                RemoteSessionRequest::Reviewer { reply, .. } => {
181                    let _ = reply.send(Err("controller daemon request bridge stopped".into()));
182                }
183            }
184            break;
185        }
186    }
187}
188
189pub(super) fn spawn_remote_actor(
190    session_id: String,
191    view: ManagedSessionView,
192    requests: &mpsc::Sender<RemoteSessionRequest>,
193    actors: &mut BTreeMap<String, RemoteActorRegistration>,
194    updates: &CoalescedUpdateSender,
195) {
196    let (actor_tx, actor_rx) = mpsc::channel(32);
197    let (release_tx, _release_rx) = mpsc::unbounded_channel();
198    let (view_tx, view_rx) = watch::channel(view.clone());
199    let abort = tokio::spawn(run_remote_session_actor(
200        session_id.clone(),
201        actor_rx,
202        requests.clone(),
203    ))
204    .abort_handle();
205    actors.insert(
206        session_id.clone(),
207        RemoteActorRegistration {
208            commands: actor_tx,
209            releases: release_tx,
210            view: view_rx,
211            view_tx,
212            abort,
213        },
214    );
215    updates.send(SessionManagerUpdate { session_id, view });
216}
217
218/// Build the read/control facade used by a control surface whose relay actors
219/// live in another process. Target updates still decide which session handles
220/// exist, while [`RemoteSessionPublisher`] supplies their latest views.
221pub fn spawn_remote_session_manager() -> Result<RemoteSessionManagerChannels> {
222    let (targets_tx, mut targets_rx) = watch::channel(Vec::<RelaySessionTarget>::new());
223    let (commands_tx, mut commands_rx) = mpsc::channel(32);
224    let (updates_tx, updates_rx) = coalesced_update_channel();
225    let (published_tx, mut published_rx) = mpsc::unbounded_channel();
226    let (requests_tx, requests_rx) = mpsc::channel(64);
227    let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
228    let task = tokio::spawn(async move {
229        let mut actors = BTreeMap::<String, RemoteActorRegistration>::new();
230        let mut latest = BTreeMap::<String, ManagedSessionView>::new();
231        let mut desired = BTreeMap::<String, RelaySessionTarget>::new();
232        loop {
233            tokio::select! {
234                _ = &mut shutdown_rx => break,
235                changed = targets_rx.changed() => {
236                    if changed.is_err() {
237                        break;
238                    }
239                    desired = target_map(&targets_rx.borrow_and_update());
240                    actors.retain(|session_id, actor| {
241                        if desired.contains_key(session_id) {
242                            true
243                        } else {
244                            actor.abort.abort();
245                            false
246                        }
247                    });
248                    // Drop the reseed view for every session that is no longer a
249                    // live target. `latest` is only ever inserted into otherwise,
250                    // so without this it keeps a full MaterializedSession per
251                    // session ever seen — a slow memory leak the actor
252                    // reconciliation above does not cover.
253                    latest.retain(|session_id, _| desired.contains_key(session_id));
254                    for session_id in desired.keys() {
255                        if !actors.contains_key(session_id)
256                            && let Some(view) = latest.get(session_id).cloned()
257                        {
258                            spawn_remote_actor(
259                                session_id.clone(),
260                                view,
261                                &requests_tx,
262                                &mut actors,
263                                &updates_tx,
264                            );
265                        }
266                    }
267                }
268                command = commands_rx.recv() => {
269                    let Some(ManagerCommand::Session { session_id, reply }) = command else {
270                        break;
271                    };
272                    let handle = actors.get(&session_id).map(|actor| ManagedSessionHandle {
273                        session_id: session_id.clone(),
274                        commands: actor.commands.clone(),
275                        releases: actor.releases.clone(),
276                        view: actor.view.clone(),
277                    });
278                    let _ = reply.send(handle);
279                }
280                published = published_rx.recv() => {
281                    let Some(RemoteManagerUpdate::Publish { session_id, view }) = published else {
282                        break;
283                    };
284                    latest.insert(session_id.clone(), view.clone());
285                    if !desired.contains_key(&session_id) {
286                        continue;
287                    }
288                    if let Some(actor) = actors.get(&session_id) {
289                        publish_view(&session_id, view, &actor.view_tx, &updates_tx);
290                        continue;
291                    }
292                    spawn_remote_actor(
293                        session_id,
294                        view,
295                        &requests_tx,
296                        &mut actors,
297                        &updates_tx,
298                    );
299                }
300            }
301        }
302        for actor in actors.into_values() {
303            actor.abort.abort();
304        }
305    });
306    Ok(RemoteSessionManagerChannels {
307        targets: targets_tx,
308        control: SessionManagerControl {
309            commands: commands_tx,
310        },
311        updates: updates_rx,
312        shutdown: SessionManagerShutdown {
313            signal: Some(shutdown_tx),
314            task: Some(task),
315        },
316        publisher: RemoteSessionPublisher {
317            updates: published_tx,
318        },
319        requests: RemoteSessionRequests {
320            requests: requests_rx,
321        },
322    })
323}