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