mj_controller/worker_client/
relay.rs1use super::*;
2
3impl RelayClient {
4 pub async fn history_requests(&mut self) -> Result<Vec<mj_core::history::HistoryRequest>> {
5 if !RelayRequest::HistoryRequests.supported_at(self.protocol_version) {
6 return Ok(Vec::new());
7 }
8 match self.call(RelayRequest::HistoryRequests).await? {
9 RelayResponsePayload::HistoryRequests { requests } => Ok(requests),
10 _ => bail!("relay returned an unexpected history queue response"),
11 }
12 }
13
14 pub async fn complete_history_request(
15 &mut self,
16 result: mj_core::history::HistoryResult,
17 ) -> Result<()> {
18 match self
19 .call(RelayRequest::CompleteHistoryRequest { result })
20 .await?
21 {
22 RelayResponsePayload::HistoryRequestCompleted => Ok(()),
23 _ => bail!("relay returned an unexpected history completion response"),
24 }
25 }
26
27 pub fn session_id(&self) -> &str {
28 &self.session_id
29 }
30
31 pub fn supports_project_memory_sync(&self) -> bool {
32 RelayRequest::ProjectMemorySnapshot.supported_at(self.protocol_version)
33 }
34
35 pub fn skills_archive_format(&self) -> mj_core::skills::SkillsArchiveFormat {
38 if self.protocol_version >= mj_core::relay::RELAY_GZIP_SKILLS_PROTOCOL {
39 mj_core::skills::SkillsArchiveFormat::Gzip
40 } else {
41 mj_core::skills::SkillsArchiveFormat::Plain
42 }
43 }
44
45 pub fn relay_version(&self) -> &str {
46 &self.relay_version
47 }
48
49 pub fn worker_build(&self) -> Option<&str> {
53 self.worker_build.as_deref()
54 }
55
56 pub fn protocol_version(&self) -> u32 {
57 self.protocol_version
58 }
59
60 pub fn latest_ordinal(&self) -> u64 {
61 self.latest_ordinal
62 }
63
64 pub fn latest_digest(&self) -> &str {
65 &self.latest_digest
66 }
67
68 pub async fn attach(
69 &mut self,
70 after_ordinal: u64,
71 after_digest: impl Into<String>,
72 ) -> Result<RelayAttachment> {
73 let after_digest = after_digest.into();
74 match self
75 .call_with_timeout(
76 RelayRequest::Attach {
77 after_ordinal,
78 after_digest: after_digest.clone(),
79 },
80 RELAY_HISTORY_TIMEOUT,
81 )
82 .await?
83 {
84 RelayResponsePayload::Attached {
85 mut state,
86 events,
87 through_ordinal,
88 through_digest,
89 } => {
90 let mut cursor = RelayCursor {
91 ordinal: after_ordinal,
92 digest: after_digest,
93 };
94 for event in &events {
95 validate_relay_event(cursor.ordinal, &cursor.digest, event)
96 .context("verify relay attachment event chain")?;
97 cursor.ordinal = event.ordinal;
98 cursor.digest.clone_from(&event.digest);
99 }
100 if cursor.ordinal != through_ordinal || cursor.digest != through_digest {
101 bail!("relay attachment frontier does not match its event chain");
102 }
103 state.relay_protocol_version = Some(self.protocol_version);
104 self.latest_ordinal = state.latest_ordinal;
105 self.latest_digest = state.latest_digest.clone();
106 Ok(RelayAttachment {
107 state,
108 events,
109 through_ordinal,
110 through_digest,
111 })
112 }
113 _ => bail!("relay returned an unexpected attach response"),
114 }
115 }
116
117 pub async fn begin_catch_up(
122 &mut self,
123 after_ordinal: u64,
124 after_digest: impl Into<String>,
125 ) -> Result<RelayCatchUp> {
126 let after_digest = after_digest.into();
127 let first = self.attach(after_ordinal, after_digest.clone()).await?;
128 let frontier = RelayCursor {
129 ordinal: first.state.latest_ordinal,
130 digest: first.state.latest_digest.clone(),
131 };
132 let previous = RelayCursor {
133 ordinal: after_ordinal,
134 digest: after_digest,
135 };
136 let state = first.state.clone();
137 let first_page = clip_catch_up_page(first, &previous, &frontier)?;
138 Ok(RelayCatchUp {
139 state,
140 frontier,
141 first_page,
142 })
143 }
144
145 pub async fn next_catch_up_page(
149 &mut self,
150 previous: &RelayCursor,
151 frontier: &RelayCursor,
152 ) -> Result<RelayEventPage> {
153 if previous.ordinal >= frontier.ordinal {
154 bail!("relay catch-up is already at its fixed frontier");
155 }
156 let attachment = self
157 .attach(previous.ordinal, previous.digest.clone())
158 .await?;
159 clip_catch_up_page(attachment, previous, frontier)
160 }
161
162 pub async fn acknowledge(
163 &mut self,
164 through_ordinal: u64,
165 through_digest: impl Into<String>,
166 ) -> Result<RelayCursor> {
167 match self
168 .call_with_timeout(
169 RelayRequest::Acknowledge {
170 through_ordinal,
171 through_digest: through_digest.into(),
172 },
173 RELAY_ACKNOWLEDGE_TIMEOUT,
174 )
175 .await?
176 {
177 RelayResponsePayload::Acknowledged {
178 through_ordinal,
179 through_digest,
180 } => Ok(RelayCursor {
181 ordinal: through_ordinal,
182 digest: through_digest,
183 }),
184 _ => bail!("relay returned an unexpected acknowledgement response"),
185 }
186 }
187
188 pub async fn status(&mut self) -> Result<RelayOperationalState> {
189 match self.call(RelayRequest::Status).await? {
190 RelayResponsePayload::Status(mut status) => {
191 status.relay_protocol_version = Some(self.protocol_version);
192 self.latest_ordinal = status.latest_ordinal;
193 self.latest_digest = status.latest_digest.clone();
194 Ok(status)
195 }
196 _ => bail!("relay returned an unexpected status response"),
197 }
198 }
199
200 pub async fn credential_state(&mut self) -> Result<CredentialSnapshot> {
203 credential_snapshot(self.call(RelayRequest::CredentialState).await?)
204 }
205
206 pub async fn read_credentials(&mut self) -> Result<Vec<u8>> {
209 match self.call(RelayRequest::ReadCredentials).await? {
210 RelayResponsePayload::Credentials { data } => BASE64
211 .decode(data.as_bytes())
212 .context("decode relay credential payload"),
213 _ => bail!("relay returned an unexpected credential response"),
214 }
215 }
216
217 pub async fn install_credentials(&mut self, bytes: &[u8]) -> Result<CredentialSnapshot> {
220 credential_snapshot(
221 self.call(RelayRequest::InstallCredentials {
222 data: BASE64.encode(bytes),
223 })
224 .await?,
225 )
226 }
227
228 pub async fn github_token_state(
229 &mut self,
230 ) -> Result<mj_core::credentials::GithubTokenSnapshot> {
231 github_token_snapshot(self.call(RelayRequest::GithubTokenState).await?)
232 }
233
234 pub async fn install_github_token(
235 &mut self,
236 token: &str,
237 ) -> Result<mj_core::credentials::GithubTokenSnapshot> {
238 github_token_snapshot(
239 self.call(RelayRequest::InstallGithubToken {
240 data: BASE64.encode(token.as_bytes()),
241 })
242 .await?,
243 )
244 }
245
246 pub async fn remove_github_token(
247 &mut self,
248 ) -> Result<mj_core::credentials::GithubTokenSnapshot> {
249 github_token_snapshot(self.call(RelayRequest::RemoveGithubToken).await?)
250 }
251
252 pub async fn skills_state(&mut self) -> Result<mj_core::skills::SkillsSyncState> {
255 skills_sync_state(self.call(RelayRequest::SkillsState).await?)
256 }
257
258 pub async fn install_prompt_context(&mut self, text: String) -> Result<()> {
261 let request = RelayRequest::InstallPromptContext { text };
262 match self.call(request).await? {
263 RelayResponsePayload::PromptContextInstalled => Ok(()),
264 _ => bail!("relay returned an unexpected prompt-context response"),
265 }
266 }
267
268 pub async fn project_memory_snapshot(
269 &mut self,
270 ) -> Result<(
271 mj_core::project_memory::ProjectMemorySnapshot,
272 mj_core::project_memory::ProjectMemorySnapshot,
273 )> {
274 let request = RelayRequest::ProjectMemorySnapshot;
275 match self.call(request).await? {
276 RelayResponsePayload::ProjectMemorySnapshot { baseline, replica } => {
277 Ok((baseline, replica))
278 }
279 _ => bail!("relay returned an unexpected project-memory response"),
280 }
281 }
282
283 pub async fn install_project_memory_snapshot(
284 &mut self,
285 snapshot: mj_core::project_memory::ProjectMemorySnapshot,
286 ) -> Result<()> {
287 let request = RelayRequest::InstallProjectMemorySnapshot { snapshot };
288 match self.call(request).await? {
289 RelayResponsePayload::ProjectMemorySnapshotInstalled => Ok(()),
290 _ => bail!("relay returned an unexpected project-memory install response"),
291 }
292 }
293
294 pub async fn install_skills(
298 &mut self,
299 archive_bytes: &[u8],
300 ) -> Result<mj_core::skills::SkillsSyncState> {
301 skills_sync_state(
302 self.call(RelayRequest::InstallSkills {
303 data: BASE64.encode(archive_bytes),
304 })
305 .await?,
306 )
307 }
308
309 pub async fn ensure_attachment(
311 &mut self,
312 reference: &mj_core::attachment::AttachmentRef,
313 ) -> Result<()> {
314 match self
315 .call(RelayRequest::AttachmentPresent {
316 reference: reference.clone(),
317 })
318 .await?
319 {
320 RelayResponsePayload::AttachmentPresent { present: true } => return Ok(()),
321 RelayResponsePayload::AttachmentPresent { present: false } => {}
322 _ => bail!("unexpected image presence response"),
323 }
324 let store = mj_core::attachment::AttachmentStore::controller(&self.session_id)?;
325 let reference_copy = reference.clone();
326 let bytes = tokio::task::spawn_blocking(move || store.read(&reference_copy))
327 .await
328 .context("image loading task failed")??;
329 match self
330 .call(RelayRequest::InstallAttachment {
331 reference: reference.clone(),
332 data: BASE64.encode(bytes),
333 })
334 .await?
335 {
336 RelayResponsePayload::AttachmentInstalled => Ok(()),
337 _ => bail!("unexpected image upload response"),
338 }
339 }
340
341 pub async fn cache_attachment(
343 &mut self,
344 reference: &mj_core::attachment::AttachmentRef,
345 ) -> Result<()> {
346 let store = mj_core::attachment::AttachmentStore::controller(&self.session_id)?;
347 let local = store.clone();
348 let reference_copy = reference.clone();
349 if tokio::task::spawn_blocking(move || local.contains(&reference_copy))
350 .await
351 .context("image lookup task failed")??
352 {
353 return Ok(());
354 }
355 let RelayResponsePayload::AttachmentData { data } = self
356 .call(RelayRequest::ReadAttachment {
357 reference: reference.clone(),
358 })
359 .await?
360 else {
361 bail!("unexpected image download response")
362 };
363 let reference = reference.clone();
364 tokio::task::spawn_blocking(move || {
365 anyhow::ensure!(
366 data.len() <= mj_core::attachment::MAX_IMAGE_BYTES.div_ceil(3) * 4,
367 "image download is too large"
368 );
369 store.install(&reference, &BASE64.decode(data)?)
370 })
371 .await
372 .context("image caching task failed")?
373 }
374
375 pub async fn submit(
376 &mut self,
377 command_id: impl Into<String>,
378 command: RelayCommand,
379 ) -> Result<u64> {
380 let command_id = command_id.into();
381 if let RelayCommand::Prompt { prompt } = &command {
382 for reference in mj_core::attachment::references(prompt)? {
383 self.ensure_attachment(&reference).await?;
384 }
385 }
386 match self
387 .call(RelayRequest::Submit {
388 command_id: command_id.clone(),
389 command,
390 })
391 .await?
392 {
393 RelayResponsePayload::Accepted {
394 command_id: accepted_id,
395 ordinal,
396 } if accepted_id == command_id => Ok(ordinal),
397 RelayResponsePayload::Accepted {
398 command_id: accepted_id,
399 ..
400 } => bail!("relay accepted command under ID {accepted_id}, expected {command_id}"),
401 _ => bail!("relay returned an unexpected command response"),
402 }
403 }
404
405 pub async fn reserve_idle(&mut self, command_id: String) -> Result<bool> {
406 match self.call(RelayRequest::ReserveIdle { command_id }).await? {
407 RelayResponsePayload::IdleReservation { ordinal } => Ok(ordinal.is_some()),
408 _ => bail!("relay returned an unexpected idle reservation response"),
409 }
410 }
411}