1use super::*;
2
3pub fn load_state() -> Result<State> {
4 load_state_from(&database_path())
5}
6
7pub fn load_state_from(path: &Path) -> Result<State> {
8 let connection = open_reader(path)?;
9 let mut state = State::default();
10 let mut statement = connection.prepare(
11 "SELECT s.session_id, s.title, s.harness_kind, s.last_profile, c.bundle_id,
12 s.target_template_id, s.state, s.native_session_id, s.acp_session_title,
13 s.session_title_override, c.created_at, s.updated_at,
14 s.viewed_through_event_ordinal, s.last_error, s.resource_allocation,
15 s.last_checkpoint_error, s.project_directory, s.managed_worktree,
16 s.draft_input, s.container_cpus, s.container_memory, s.archived
17 , c.workspace_id, s.create_managed_worktree, s.mjolnir_subagents,
18 s.container_workspace, s.build_cache_json
19 FROM sessions s JOIN session_contexts c USING(session_id)
20 ORDER BY s.session_id",
21 )?;
22 let rows = statement.query_map([], |row| {
23 let harness_text: String = row.get(2)?;
27 let Ok(harness_kind) = harness_text.parse() else {
28 let session_id: String = row.get(0)?;
29 tracing::warn!(
30 session_id,
31 harness = %harness_text,
32 "session harness is no longer supported; the session is not listed"
33 );
34 return Ok(None);
35 };
36 Ok(Some(SessionRecord {
37 harness_kind,
38 create_managed_worktree: row.get(23)?,
39 mjolnir_subagents: row.get(24)?,
40 container_workspace: row.get::<_, Option<String>>(25)?.map(PathBuf::from),
41 build_cache: row
42 .get::<_, Option<String>>(26)?
43 .as_deref()
44 .and_then(|text| match serde_json::from_str(text) {
45 Ok(build_cache) => Some(build_cache),
46 Err(error) => {
47 tracing::warn!(%error, "session build cache record is unreadable");
48 None
49 }
50 }),
51 workspace_id: row.get(22)?,
52 archived: row.get(21)?,
53 container_cpus: row.get(19)?,
54 container_memory: row.get(20)?,
55 id: row.get(0)?,
56 title: row.get(1)?,
57 last_profile: row.get(3)?,
58 bundle_id: row.get(4)?,
59 project_directory: row.get_ref(16)?.blob_or_null()?.map(blob_to_path),
60 managed_worktree: row
61 .get::<_, Option<String>>(17)?
62 .map(|json| serde_json::from_str::<ManagedWorktree>(&json))
63 .transpose()
64 .map_err(|error| {
65 rusqlite::Error::FromSqlConversionFailure(
66 17,
67 rusqlite::types::Type::Text,
68 Box::new(error),
69 )
70 })?,
71 target_template_id: row.get(5)?,
72 resource_allocation: row
73 .get::<_, Option<String>>(14)?
74 .map(|json| serde_json::from_str::<SessionResourceAllocation>(&json))
75 .transpose()
76 .map_err(|error| {
77 rusqlite::Error::FromSqlConversionFailure(
78 14,
79 rusqlite::types::Type::Text,
80 Box::new(error),
81 )
82 })?,
83 additional_mounts: Vec::new(),
84 state: stored_session_state(&row.get::<_, String>(6)?),
85 target: None,
86 native_session_id: row.get(7)?,
87 acp_session_title: row
88 .get::<_, Option<String>>(8)?
89 .as_deref()
90 .and_then(mj_core::state::normalize_session_title),
91 session_title_override: row.get(9)?,
92 created_at: row.get(10)?,
93 updated_at: row.get(11)?,
94 viewed_through_event_ordinal: row.get::<_, u64>(12)?,
95 draft_input: row.get(18)?,
96 last_error: row.get(13)?,
97 last_checkpoint_error: row.get(15)?,
98 checkpoint: None,
99 }))
100 })?;
101 for row in rows {
102 if let Some(session) = row? {
103 state.sessions.insert(session.id.clone(), session);
104 }
105 }
106 let mut statement = connection.prepare(
107 "SELECT child_session_id, record_json FROM subagent_sessions ORDER BY child_session_id",
108 )?;
109 let rows = statement.query_map([], |row| {
110 let child_id = row.get::<_, String>(0)?;
111 let json = row.get::<_, String>(1)?;
112 let record = serde_json::from_str::<SubagentRecord>(&json).map_err(|error| {
113 rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error))
114 })?;
115 Ok((child_id, record))
116 })?;
117 for row in rows {
118 let (child_id, record) = row?;
119 state.subagents.insert(child_id, record);
120 }
121 load_targets(&connection, &mut state)?;
122 load_mounts(&connection, &mut state)?;
123 load_checkpoints(&connection, &mut state)?;
124 let mut statement =
125 connection.prepare("SELECT host, source FROM mount_history ORDER BY host, ordinal")?;
126 let rows = statement.query_map([], |row| {
127 Ok((
128 row.get::<_, String>(0)?,
129 blob_to_path(row.get_ref(1)?.as_blob()?),
130 ))
131 })?;
132 for row in rows {
133 let (host, source) = row?;
134 state.mount_history.entry(host).or_default().push(source);
135 }
136 let mut statement = connection
137 .prepare("SELECT host, cpus, memory_bytes FROM host_container_sizes ORDER BY host")?;
138 let rows = statement.query_map([], |row| {
139 Ok((
140 row.get::<_, String>(0)?,
141 HostContainerSize {
142 cpus: row.get::<_, i64>(1)? as u64,
143 memory_bytes: row.get::<_, i64>(2)? as u64,
144 },
145 ))
146 })?;
147 for row in rows {
148 let (host, size) = row?;
149 state.container_sizes.insert(host, size);
150 }
151 state.validate()?;
152 Ok(state)
153}
154
155pub fn save_state(state: &State) -> Result<()> {
156 let state = state.clone();
157 submit_database_write("save_state", move |_| {
158 save_state_to(&database_path(), &state)
159 })
160}
161
162pub fn save_state_to(path: &Path, state: &State) -> Result<()> {
163 state.validate()?;
164 let mut connection = open(path)?;
165 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
166 let existing_contexts = existing_contexts(&tx)?;
167 let existing_sessions = {
168 let mut statement = tx.prepare("SELECT session_id FROM sessions")?;
169 statement
170 .query_map([], |row| row.get::<_, String>(0))?
171 .collect::<rusqlite::Result<Vec<_>>>()?
172 };
173 tx.execute(
174 "DELETE FROM subagent_sessions
175 WHERE child_session_id NOT IN (SELECT session_id FROM sessions)
176 OR parent_session_id NOT IN (SELECT session_id FROM sessions)",
177 [],
178 )?;
179 let existing_subagents = {
180 let mut statement =
181 tx.prepare("SELECT child_session_id, parent_session_id FROM subagent_sessions")?;
182 statement
183 .query_map([], |row| {
184 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
185 })?
186 .collect::<rusqlite::Result<Vec<_>>>()?
187 };
188 for (child_id, parent_id) in existing_subagents {
189 if !state.subagents.contains_key(&child_id)
190 || !state.sessions.contains_key(&child_id)
191 || !state.sessions.contains_key(&parent_id)
192 {
193 tx.execute(
194 "DELETE FROM subagent_sessions WHERE child_session_id = ?1",
195 [child_id],
196 )?;
197 }
198 }
199 for session_id in existing_sessions {
200 if !state.sessions.contains_key(&session_id) {
201 tx.execute("DELETE FROM sessions WHERE session_id = ?1", [session_id])?;
202 }
203 }
204 tx.execute("DELETE FROM mount_history", [])?;
205 tx.execute("DELETE FROM host_container_sizes", [])?;
206 for session in state.sessions.values() {
207 if let Some((existing_bundle, existing_workspace)) = existing_contexts.get(&session.id) {
208 ensure!(
209 existing_bundle == &session.bundle_id,
210 "session {} was already associated with bundle {}, not {}",
211 session.id,
212 existing_bundle,
213 session.bundle_id
214 );
215 ensure!(
216 existing_workspace == &session.workspace_id,
217 "session {} was already associated with workspace {}, not {}",
218 session.id,
219 existing_workspace,
220 session.workspace_id
221 );
222 }
223 insert_session(&tx, session)?;
224 }
225 for subagent in state.subagents.values() {
226 let record_json = serde_json::to_string(subagent)?;
227 tx.execute(
228 "INSERT INTO subagent_sessions(
229 child_session_id, parent_session_id, request_key, record_json
230 ) VALUES (?1, ?2, ?3, ?4)
231 ON CONFLICT(child_session_id) DO UPDATE SET
232 parent_session_id = excluded.parent_session_id,
233 request_key = excluded.request_key,
234 record_json = excluded.record_json",
235 params![
236 subagent.child_session_id,
237 subagent.parent_session_id,
238 subagent.request_key,
239 record_json
240 ],
241 )?;
242 }
243 for (host, sources) in &state.mount_history {
244 for (ordinal, source) in sources.iter().enumerate() {
245 tx.execute(
246 "INSERT INTO mount_history(host, source, ordinal) VALUES (?1, ?2, ?3)",
247 params![host, path_to_blob(source), ordinal as i64],
248 )?;
249 }
250 }
251 for (host, size) in &state.container_sizes {
252 write_host_container_size(&tx, host, *size)?;
253 }
254 tx.commit()?;
255 Ok(())
256}
257
258pub(super) fn existing_contexts(
259 tx: &Transaction<'_>,
260) -> Result<BTreeMap<String, (String, String)>> {
261 let mut statement =
262 tx.prepare("SELECT session_id, bundle_id, workspace_id FROM session_contexts")?;
263 let rows = statement.query_map([], |row| Ok((row.get(0)?, (row.get(1)?, row.get(2)?))))?;
264 rows.collect::<rusqlite::Result<_>>().map_err(Into::into)
265}
266
267pub(super) fn session_exists(tx: &Transaction<'_>, session_id: &str) -> Result<bool> {
268 Ok(tx
269 .query_row(
270 "SELECT 1 FROM sessions WHERE session_id = ?1",
271 [session_id],
272 |_| Ok(()),
273 )
274 .optional()?
275 .is_some())
276}
277
278pub(super) fn write_materialized_session(
279 tx: &Transaction<'_>,
280 materialized: &MaterializedSession,
281) -> Result<()> {
282 let (execution, running_started_at_ms) = materialized_execution_columns(materialized.execution);
283 tx.execute(
284 "INSERT INTO materialized_sessions(
285 session_id, applied_event_ordinal, applied_event_digest, execution_state,
286 running_started_at_ms, session_title, configuration_json, last_activity_at_ms,
287 pending_elicitations_json, active_turn_json, last_turn_outcome_json
288 ) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)
289 ON CONFLICT(session_id) DO UPDATE SET
290 applied_event_ordinal = excluded.applied_event_ordinal,
291 applied_event_digest = excluded.applied_event_digest,
292 execution_state = excluded.execution_state,
293 running_started_at_ms = excluded.running_started_at_ms,
294 session_title = excluded.session_title,
295 configuration_json = excluded.configuration_json,
296 last_activity_at_ms = excluded.last_activity_at_ms,
297 pending_elicitations_json = excluded.pending_elicitations_json,
298 active_turn_json = excluded.active_turn_json,
299 last_turn_outcome_json = excluded.last_turn_outcome_json",
300 params![
301 materialized.session_id,
302 materialized.applied_event_ordinal,
303 materialized.applied_event_digest,
304 execution,
305 running_started_at_ms,
306 materialized.session_title,
307 serde_json::to_string(&materialized.configuration)?,
308 materialized.last_activity_at_ms,
309 serde_json::to_string(&materialized.pending_elicitations)?,
310 materialized
311 .active_turn
312 .as_ref()
313 .map(serde_json::to_string)
314 .transpose()?,
315 materialized
316 .last_turn_outcome
317 .as_ref()
318 .map(serde_json::to_string)
319 .transpose()?,
320 ],
321 )?;
322 tx.execute(
323 "DELETE FROM materialized_transcript_items WHERE session_id = ?1",
324 [materialized.session_id.as_str()],
325 )?;
326 for item in &materialized.transcript {
327 upsert_transcript_item(tx, &materialized.session_id, item)?;
328 }
329 replace_materialized_queue(tx, &materialized.session_id, &materialized.queued_prompts)?;
330 Ok(())
331}
332
333pub(super) fn upsert_transcript_item(
334 tx: &Transaction<'_>,
335 session_id: &str,
336 item: &TranscriptItem,
337) -> Result<()> {
338 let existing = tx
339 .query_row(
340 "SELECT position, latest_content_event_ordinal, created_at_ms, last_changed_at_ms
341 FROM materialized_transcript_items
342 WHERE session_id = ?1 AND stable_id = ?2",
343 params![session_id, item.stable_id],
344 |row| {
345 Ok((
346 row.get::<_, u64>(0)?,
347 row.get::<_, Option<u64>>(1)?,
348 row.get::<_, i64>(2)?,
349 row.get::<_, i64>(3)?,
350 ))
351 },
352 )
353 .optional()?;
354 if let Some((position, latest_content_event_ordinal, created_at_ms, last_changed_at_ms)) =
355 existing
356 {
357 if position != item.position || created_at_ms != item.created_at_ms {
358 return Err(ProjectionIntegrityError(format!(
359 "transcript item {:?} changed immutable identity fields",
360 item.stable_id
361 ))
362 .into());
363 }
364 if item.last_changed_at_ms < last_changed_at_ms {
365 return Err(ProjectionIntegrityError(format!(
366 "transcript item {:?} moved its changed timestamp backwards",
367 item.stable_id
368 ))
369 .into());
370 }
371 if latest_content_event_ordinal.is_some_and(|existing| {
372 item.latest_content_event_ordinal
373 .is_none_or(|next| next < existing)
374 }) {
375 return Err(ProjectionIntegrityError(format!(
376 "transcript item {:?} moved its latest content ordinal backwards",
377 item.stable_id
378 ))
379 .into());
380 }
381 tx.execute(
382 "UPDATE materialized_transcript_items
383 SET latest_content_event_ordinal = ?3, last_changed_at_ms = ?4, body_json = ?5
384 WHERE session_id = ?1 AND stable_id = ?2",
385 params![
386 session_id,
387 item.stable_id,
388 item.latest_content_event_ordinal,
389 item.last_changed_at_ms,
390 serde_json::to_string(&item.body)?,
391 ],
392 )?;
393 } else {
394 tx.execute(
395 "INSERT INTO materialized_transcript_items(
396 session_id, stable_id, position, latest_content_event_ordinal,
397 created_at_ms, last_changed_at_ms, body_json
398 ) VALUES (?1,?2,?3,?4,?5,?6,?7)",
399 params![
400 session_id,
401 item.stable_id,
402 item.position,
403 item.latest_content_event_ordinal,
404 item.created_at_ms,
405 item.last_changed_at_ms,
406 serde_json::to_string(&item.body)?,
407 ],
408 )?;
409 }
410 Ok(())
411}
412
413pub(super) fn replace_materialized_queue(
414 tx: &Transaction<'_>,
415 session_id: &str,
416 queued_prompts: &[MaterializedQueuedPrompt],
417) -> Result<()> {
418 let mut command_ids = BTreeSet::new();
419 for prompt in queued_prompts {
420 if prompt.command_id.trim().is_empty() {
421 bail!("materialized prompt queue has an empty command id");
422 }
423 if !command_ids.insert(prompt.command_id.as_str()) {
424 bail!(
425 "materialized prompt queue contains duplicate command {:?}",
426 prompt.command_id
427 );
428 }
429 }
430 tx.execute(
431 "DELETE FROM materialized_queued_prompts WHERE session_id = ?1",
432 [session_id],
433 )?;
434 for (ordinal, prompt) in queued_prompts.iter().enumerate() {
435 tx.execute(
436 "INSERT INTO materialized_queued_prompts(
437 session_id, ordinal, command_id, kind_json, content_json, queued_at_ms,
438 accepted_ordinal
439 ) VALUES (?1,?2,?3,?4,?5,?6,?7)",
440 params![
441 session_id,
442 ordinal as i64,
443 prompt.command_id,
444 serde_json::to_string(&prompt.kind)?,
445 serde_json::to_string(&prompt.content)?,
446 prompt.queued_at_ms,
447 prompt.accepted_ordinal,
448 ],
449 )?;
450 }
451 Ok(())
452}
453
454pub(super) fn materialized_execution_columns(
455 execution: MaterializedExecutionState,
456) -> (&'static str, Option<i64>) {
457 match execution {
458 MaterializedExecutionState::Idle => ("idle", None),
459 MaterializedExecutionState::Running { started_at_ms } => ("running", Some(started_at_ms)),
460 MaterializedExecutionState::Closing => ("closing", None),
461 MaterializedExecutionState::Closed => ("closed", None),
462 }
463}
464
465pub(super) fn parse_materialized_execution(
466 execution: &str,
467 running_started_at_ms: Option<i64>,
468) -> Result<MaterializedExecutionState> {
469 match (execution, running_started_at_ms) {
470 ("idle", None) => Ok(MaterializedExecutionState::Idle),
471 ("running", Some(started_at_ms)) => {
472 Ok(MaterializedExecutionState::Running { started_at_ms })
473 }
474 ("closing", None) => Ok(MaterializedExecutionState::Closing),
475 ("closed", None) => Ok(MaterializedExecutionState::Closed),
476 _ => bail!("invalid materialized execution state {execution:?}"),
477 }
478}
479
480pub(super) fn insert_session(tx: &Transaction<'_>, session: &SessionRecord) -> Result<()> {
484 tx.execute(
485 "INSERT INTO session_contexts(session_id, bundle_id, created_at, workspace_id)
486 VALUES (?1, ?2, ?3, ?4)
487 ON CONFLICT(session_id) DO NOTHING",
488 params![
489 session.id,
490 session.bundle_id,
491 session.created_at,
492 session.workspace_id
493 ],
494 )?;
495 let (stored_bundle, stored_workspace): (String, String) = tx.query_row(
496 "SELECT bundle_id, workspace_id FROM session_contexts WHERE session_id = ?1",
497 [session.id.as_str()],
498 |row| Ok((row.get(0)?, row.get(1)?)),
499 )?;
500 ensure!(
501 stored_bundle == session.bundle_id,
502 "session {} belongs to bundle {}, not {}",
503 session.id,
504 stored_bundle,
505 session.bundle_id
506 );
507 ensure!(
508 stored_workspace == session.workspace_id,
509 "session {} belongs to workspace {}, not {}",
510 session.id,
511 stored_workspace,
512 session.workspace_id
513 );
514 tx.execute(
515 "INSERT INTO sessions(
516 session_id, title, harness_kind, last_profile, target_template_id, state,
517 native_session_id, acp_session_title, session_title_override, updated_at,
518 viewed_through_event_ordinal, last_error, resource_allocation,
519 last_checkpoint_error, project_directory, managed_worktree,
520 container_cpus, container_memory, archived, draft_input, create_managed_worktree,
521 mjolnir_subagents, container_workspace, build_cache_json
522 ) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24)
523 ON CONFLICT(session_id) DO UPDATE SET
524 title = excluded.title,
525 harness_kind = excluded.harness_kind,
526 last_profile = excluded.last_profile,
527 target_template_id = excluded.target_template_id,
528 state = excluded.state,
529 native_session_id = excluded.native_session_id,
530 acp_session_title = excluded.acp_session_title,
531 session_title_override = excluded.session_title_override,
532 updated_at = excluded.updated_at,
533 viewed_through_event_ordinal = max(
534 sessions.viewed_through_event_ordinal,
535 excluded.viewed_through_event_ordinal
536 ),
537 last_error = excluded.last_error,
538 resource_allocation = excluded.resource_allocation,
539 last_checkpoint_error = excluded.last_checkpoint_error,
540 project_directory = excluded.project_directory,
541 managed_worktree = excluded.managed_worktree,
542 container_cpus = excluded.container_cpus,
543 container_memory = excluded.container_memory,
544 archived = excluded.archived,
545 create_managed_worktree = excluded.create_managed_worktree,
546 mjolnir_subagents = excluded.mjolnir_subagents,
547 container_workspace = excluded.container_workspace,
548 build_cache_json = excluded.build_cache_json",
549 params![
550 session.id,
551 session.title,
552 session.harness_kind.id(),
553 session.last_profile,
554 session.target_template_id,
555 session.state.as_str(),
556 session.native_session_id,
557 session.acp_session_title,
558 session.session_title_override,
559 session.updated_at,
560 session.viewed_through_event_ordinal,
561 session.last_error,
562 session
563 .resource_allocation
564 .as_ref()
565 .map(serde_json::to_string)
566 .transpose()?,
567 session.last_checkpoint_error,
568 session
569 .project_directory
570 .as_ref()
571 .map(|path| path_to_blob(path)),
572 session
573 .managed_worktree
574 .as_ref()
575 .map(serde_json::to_string)
576 .transpose()?,
577 session.container_cpus,
578 session.container_memory,
579 session.archived,
580 session.draft_input,
581 session.create_managed_worktree,
582 session.mjolnir_subagents,
583 session
584 .container_workspace
585 .as_ref()
586 .map(|path| path.to_string_lossy().into_owned()),
587 session
588 .build_cache
589 .as_ref()
590 .map(serde_json::to_string)
591 .transpose()?,
592 ],
593 )?;
594 tx.execute(
595 "INSERT INTO materialized_sessions(session_id) VALUES (?1)
596 ON CONFLICT(session_id) DO NOTHING",
597 [session.id.as_str()],
598 )?;
599 replace_targets(tx, session)?;
600 replace_mounts(tx, &session.id, &session.additional_mounts)?;
601 replace_checkpoint(tx, session)?;
602 Ok(())
603}
604
605pub(super) fn update_lifecycle_fields(tx: &Transaction<'_>, session: &SessionRecord) -> Result<()> {
609 let changed = tx.execute(
610 "UPDATE sessions
613 SET title = ?2,
614 harness_kind = ?3,
615 last_profile = ?4,
616 target_template_id = ?5,
617 state = ?6,
618 updated_at = ?7,
619 viewed_through_event_ordinal = max(viewed_through_event_ordinal, ?8),
620 last_error = ?9,
621 resource_allocation = ?10,
622 last_checkpoint_error = ?11,
623 project_directory = ?12,
624 managed_worktree = ?13
625 WHERE session_id = ?1",
626 params![
627 session.id,
628 session.title,
629 session.harness_kind.id(),
630 session.last_profile,
631 session.target_template_id,
632 session.state.as_str(),
633 session.updated_at,
634 session.viewed_through_event_ordinal,
635 session.last_error,
636 session
637 .resource_allocation
638 .as_ref()
639 .map(serde_json::to_string)
640 .transpose()?,
641 session.last_checkpoint_error,
642 session
643 .project_directory
644 .as_ref()
645 .map(|path| path_to_blob(path)),
646 session
647 .managed_worktree
648 .as_ref()
649 .map(serde_json::to_string)
650 .transpose()?,
651 ],
652 )?;
653 if changed != 1 {
654 bail!("unknown session {}", session.id);
655 }
656 replace_targets(tx, session)
657}
658
659pub(super) fn replace_targets(tx: &Transaction<'_>, session: &SessionRecord) -> Result<()> {
660 tx.execute(
661 "DELETE FROM session_targets WHERE session_id = ?1",
662 [session.id.as_str()],
663 )?;
664 if let Some(target) = &session.target {
665 insert_target(tx, &session.id, target)?;
666 }
667 Ok(())
668}
669
670pub(super) fn replace_checkpoint(tx: &Transaction<'_>, session: &SessionRecord) -> Result<()> {
671 tx.execute(
672 "DELETE FROM session_checkpoints WHERE session_id = ?1",
673 [session.id.as_str()],
674 )?;
675 if let Some(checkpoint) = &session.checkpoint {
676 tx.execute(
677 "INSERT INTO session_checkpoints(session_id, archive_path, sha256, created_at, event_frontier)
678 VALUES (?1, ?2, ?3, ?4, ?5)",
679 params![
680 session.id,
681 path_to_blob(&checkpoint.archive_path),
682 checkpoint.sha256,
683 checkpoint.created_at,
684 checkpoint.event_frontier,
685 ],
686 )?;
687 }
688 Ok(())
689}
690
691pub(super) fn insert_target(
692 tx: &Transaction<'_>,
693 session_id: &str,
694 target: &TargetLocator,
695) -> Result<()> {
696 let (kind, host, resource, address, workspace, worker_id, workspace_storage, borrowed_from) =
697 match target {
698 TargetLocator::LocalBare { worker_root } => (
699 "local-bare",
700 None,
701 None,
702 None,
703 Some(path_to_blob(worker_root)),
704 None,
705 None,
706 None,
707 ),
708 TargetLocator::LocalPodman {
709 container_id,
710 workspace_storage,
711 borrowed_from,
712 } => (
713 "local-podman",
714 None,
715 Some(container_id.as_str()),
716 None,
717 None,
718 None,
719 Some(serde_json::to_string(workspace_storage)?),
720 borrowed_from.as_deref(),
721 ),
722 TargetLocator::LocalDocker {
723 container_id,
724 borrowed_from,
725 } => (
726 "local-docker",
727 None,
728 Some(container_id.as_str()),
729 None,
730 None,
731 None,
732 None,
733 borrowed_from.as_deref(),
734 ),
735 TargetLocator::SshDocker {
736 host,
737 container_id,
738 borrowed_from,
739 } => (
740 "ssh-docker",
741 Some(host.as_str()),
742 Some(container_id.as_str()),
743 None,
744 None,
745 None,
746 None,
747 borrowed_from.as_deref(),
748 ),
749 TargetLocator::AppleContainer {
750 container_id,
751 borrowed_from,
752 } => (
753 "apple-container",
754 None,
755 Some(container_id.as_str()),
756 None,
757 None,
758 None,
759 None,
760 borrowed_from.as_deref(),
761 ),
762 TargetLocator::AwsEc2 {
763 instance_id,
764 address,
765 } => (
766 "aws-ec2",
767 None,
768 Some(instance_id.as_str()),
769 address.as_deref(),
770 None,
771 None,
772 None,
773 None,
774 ),
775 TargetLocator::SshBare {
776 host,
777 workspace,
778 worker_id,
779 } => (
780 "ssh-bare",
781 Some(host.as_str()),
782 None,
783 None,
784 Some(path_to_blob(workspace)),
785 worker_id.as_deref(),
786 None,
787 None,
788 ),
789 TargetLocator::SshPodman {
790 host,
791 container_id,
792 workspace_storage,
793 borrowed_from,
794 } => (
795 "ssh-podman",
796 Some(host.as_str()),
797 Some(container_id.as_str()),
798 None,
799 None,
800 None,
801 Some(serde_json::to_string(workspace_storage)?),
802 borrowed_from.as_deref(),
803 ),
804 };
805 tx.execute(
806 "INSERT INTO session_targets(session_id, kind, host, resource_id, address, workspace, worker_id, workspace_storage, borrowed_from)
807 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
808 params![
809 session_id,
810 kind,
811 host,
812 resource,
813 address,
814 workspace,
815 worker_id,
816 workspace_storage,
817 borrowed_from
818 ],
819 )?;
820 Ok(())
821}
822
823pub(super) fn load_targets(connection: &Connection, state: &mut State) -> Result<()> {
824 let mut statement = connection.prepare(
825 "SELECT session_id, kind, host, resource_id, address, workspace, worker_id, workspace_storage, borrowed_from
826 FROM session_targets",
827 )?;
828 let rows = statement.query_map([], |row| {
829 let session_id: String = row.get(0)?;
830 let kind: String = row.get(1)?;
831 let host: Option<String> = row.get(2)?;
832 let resource: Option<String> = row.get(3)?;
833 let address: Option<String> = row.get(4)?;
834 let workspace = row.get_ref(5)?.blob_or_null()?.map(blob_to_path);
835 let worker_id: Option<String> = row.get(6)?;
836 let workspace_storage = row
837 .get::<_, Option<String>>(7)?
838 .map(|serialized| {
839 serde_json::from_str(&serialized).map_err(|error| {
840 rusqlite::Error::FromSqlConversionFailure(7, Type::Text, Box::new(error))
841 })
842 })
843 .transpose()?
844 .unwrap_or_default();
845 let borrowed_from: Option<String> = row.get(8)?;
846 let target = match kind.as_str() {
847 "local-bare" => TargetLocator::LocalBare {
848 worker_root: workspace.unwrap(),
849 },
850 "local-podman" => TargetLocator::LocalPodman {
851 borrowed_from,
852 container_id: resource.unwrap(),
853 workspace_storage,
854 },
855 "local-docker" => TargetLocator::LocalDocker {
856 borrowed_from,
857 container_id: resource.unwrap(),
858 },
859 "apple-container" => TargetLocator::AppleContainer {
860 borrowed_from,
861 container_id: resource.unwrap(),
862 },
863 "aws-ec2" => TargetLocator::AwsEc2 {
864 instance_id: resource.unwrap(),
865 address,
866 },
867 "ssh-bare" => TargetLocator::SshBare {
868 host: host.unwrap(),
869 workspace: workspace.unwrap(),
870 worker_id,
871 },
872 "ssh-docker" => TargetLocator::SshDocker {
873 borrowed_from,
874 host: host.unwrap(),
875 container_id: resource.unwrap(),
876 },
877 "ssh-podman" => TargetLocator::SshPodman {
878 borrowed_from,
879 host: host.unwrap(),
880 container_id: resource.unwrap(),
881 workspace_storage,
882 },
883 _ => unreachable!("target kind constrained by schema"),
884 };
885 Ok((session_id, target))
886 })?;
887 for row in rows {
888 let (session_id, target) = row?;
889 if let Some(session) = state.sessions.get_mut(&session_id) {
892 session.target = Some(target);
893 }
894 }
895 Ok(())
896}
897
898pub(super) fn replace_mounts(
905 tx: &rusqlite::Transaction<'_>,
906 session_id: &str,
907 mounts: &[AdditionalMount],
908) -> Result<()> {
909 tx.execute(
910 "DELETE FROM session_mounts WHERE session_id = ?1",
911 [session_id],
912 )?;
913 tx.execute(
914 "DELETE FROM session_mount_access WHERE session_id = ?1",
915 [session_id],
916 )?;
917 for (ordinal, mount) in mounts.iter().enumerate() {
918 tx.execute(
919 "INSERT INTO session_mounts(session_id, ordinal, source, destination, read_only)
920 VALUES (?1, ?2, ?3, ?4, ?5)",
921 params![
922 session_id,
923 ordinal as i64,
924 path_to_blob(&mount.source),
925 path_to_blob(&mount.destination),
926 mount.access == MountAccess::Ro
927 ],
928 )?;
929 if mount.access == MountAccess::Rw {
930 tx.execute(
931 "INSERT INTO session_mount_access(session_id, source, destination, access)
932 VALUES (?1, ?2, ?3, 'rw')",
933 params![
934 session_id,
935 path_to_blob(&mount.source),
936 path_to_blob(&mount.destination)
937 ],
938 )?;
939 }
940 }
941 Ok(())
942}
943
944pub(super) fn load_mounts(connection: &Connection, state: &mut State) -> Result<()> {
945 let mut statement = connection.prepare(
946 "SELECT m.session_id, m.source, m.destination, m.read_only, a.access IS NOT NULL
947 FROM session_mounts m
948 LEFT JOIN session_mount_access a
949 ON a.session_id = m.session_id
950 AND a.source = m.source
951 AND a.destination = m.destination
952 ORDER BY m.session_id, m.ordinal",
953 )?;
954 let rows = statement.query_map([], |row| {
955 let access = match (row.get::<_, bool>(3)?, row.get::<_, bool>(4)?) {
958 (true, _) => MountAccess::Ro,
959 (false, true) => MountAccess::Rw,
960 (false, false) => MountAccess::Cow,
961 };
962 Ok((
963 row.get::<_, String>(0)?,
964 AdditionalMount {
965 source: blob_to_path(row.get_ref(1)?.as_blob()?),
966 destination: blob_to_path(row.get_ref(2)?.as_blob()?),
967 access,
968 },
969 ))
970 })?;
971 for row in rows {
972 let (session_id, mount) = row?;
973 if let Some(session) = state.sessions.get_mut(&session_id) {
974 session.additional_mounts.push(mount);
975 }
976 }
977 Ok(())
978}
979
980pub(super) fn load_checkpoints(connection: &Connection, state: &mut State) -> Result<()> {
981 let mut statement = connection.prepare(
982 "SELECT session_id, archive_path, sha256, created_at, event_frontier FROM session_checkpoints",
983 )?;
984 let rows = statement.query_map([], |row| {
985 Ok((
986 row.get::<_, String>(0)?,
987 CheckpointMetadata {
988 archive_path: blob_to_path(row.get_ref(1)?.as_blob()?),
989 sha256: row.get(2)?,
990 created_at: row.get(3)?,
991 event_frontier: row.get(4)?,
992 },
993 ))
994 })?;
995 for row in rows {
996 let (session_id, checkpoint) = row?;
997 if let Some(session) = state.sessions.get_mut(&session_id) {
998 session.checkpoint = Some(checkpoint);
999 }
1000 }
1001 Ok(())
1002}