1use super::*;
2
3pub fn save_session(session: &SessionRecord) -> Result<()> {
7 let session = session.clone();
8 submit_database_write("save_session", move |_| {
9 save_session_to(&database_path(), &session)
10 })
11}
12
13pub fn save_subagent_session(
15 session: &SessionRecord,
16 subagent: &mj_core::subagent::SubagentRecord,
17) -> Result<()> {
18 let session = session.clone();
19 let subagent = subagent.clone();
20 submit_database_write("save_subagent_session", move |_| {
21 let mut connection = open(&database_path())?;
22 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
23 insert_session(&tx, &session)?;
24 tx.execute(
25 "INSERT INTO subagent_sessions(
26 child_session_id, parent_session_id, request_key, record_json
27 ) VALUES (?1, ?2, ?3, ?4)",
28 params![
29 subagent.child_session_id,
30 subagent.parent_session_id,
31 subagent.request_key,
32 serde_json::to_string(&subagent)?,
33 ],
34 )?;
35 tx.commit()?;
36 Ok(())
37 })
38}
39
40pub fn mark_subagent_turn_noticed(child_session_id: &str, turn: u64) -> Result<()> {
42 let child_session_id = child_session_id.to_owned();
43 submit_database_write("mark_subagent_turn_noticed", move |_| {
44 let mut relation = load_subagent(&child_session_id)?
45 .with_context(|| format!("unknown sub-agent session {child_session_id}"))?;
46 relation.noticed_turn = Some(turn);
47 let json = serde_json::to_string(&relation)?;
48 let connection = open(&database_path())?;
49 connection.execute(
50 "UPDATE subagent_sessions SET record_json = ?2 WHERE child_session_id = ?1",
51 params![child_session_id, json],
52 )?;
53 Ok(())
54 })
55}
56
57pub fn load_subagent(child_session_id: &str) -> Result<Option<mj_core::subagent::SubagentRecord>> {
58 let connection = open_reader(&database_path())?;
59 connection
60 .query_row(
61 "SELECT record_json FROM subagent_sessions WHERE child_session_id = ?1",
62 [child_session_id],
63 |row| row.get::<_, String>(0),
64 )
65 .optional()?
66 .map(|json| serde_json::from_str(&json).context("decode sub-agent record"))
67 .transpose()
68}
69
70pub fn list_subagents(parent_session_id: &str) -> Result<Vec<mj_core::subagent::SubagentRecord>> {
71 let connection = open_reader(&database_path())?;
72 let mut statement = connection.prepare(
73 "SELECT record_json FROM subagent_sessions
74 WHERE parent_session_id = ?1 ORDER BY rowid",
75 )?;
76 statement
77 .query_map([parent_session_id], |row| row.get::<_, String>(0))?
78 .map(|row| serde_json::from_str(&row?).context("decode sub-agent record"))
79 .collect()
80}
81
82pub fn lookup_subagent_request(
83 parent_session_id: &str,
84 request_key: &str,
85) -> Result<Option<mj_core::subagent::SubagentRecord>> {
86 let connection = open_reader(&database_path())?;
87 connection
88 .query_row(
89 "SELECT record_json FROM subagent_sessions
90 WHERE parent_session_id = ?1 AND request_key = ?2",
91 params![parent_session_id, request_key],
92 |row| row.get::<_, String>(0),
93 )
94 .optional()?
95 .map(|json| serde_json::from_str(&json).context("decode sub-agent record"))
96 .transpose()
97}
98
99pub fn save_session_with_container_size(
102 session: &SessionRecord,
103 host: &str,
104 size: HostContainerSize,
105) -> Result<()> {
106 let session = session.clone();
107 let host = host.to_owned();
108 submit_database_write("save_session_with_container_size", move |_| {
109 save_session_with_container_size_to(&database_path(), &session, Some((&host, size)))
110 })
111}
112
113pub fn save_lifecycle_session(session: &SessionRecord) -> Result<()> {
117 let session = session.clone();
118 submit_database_write("save_lifecycle_session", move |_| {
119 save_lifecycle_session_to(&database_path(), &session)
120 })
121}
122
123pub fn save_checkpointed_session(session: &SessionRecord) -> Result<()> {
126 let session = session.clone();
127 submit_database_write("save_checkpointed_session", move |_| {
128 save_checkpointed_session_to(&database_path(), &session)
129 })
130}
131
132pub fn recover_interrupted_checkpointing_sessions(updated_at: &str) -> Result<usize> {
136 let updated_at = updated_at.to_owned();
137 submit_database_write("recover_interrupted_checkpointing_sessions", move |_| {
138 recover_interrupted_checkpointing_sessions_to(&database_path(), &updated_at)
139 })
140}
141
142pub fn set_session_title_override(session_id: &str, title: &str, updated_at: &str) -> Result<()> {
145 let session_id = session_id.to_owned();
146 let title = title.to_owned();
147 let updated_at = updated_at.to_owned();
148 submit_database_write("set_session_title_override", move |_| {
149 set_session_title_override_to(&database_path(), &session_id, &title, &updated_at)
150 })
151}
152
153pub fn rename_profile_references(old_id: &str, new_id: &str) -> Result<usize> {
157 rename_session_reference("last_profile", old_id, new_id)
158}
159
160pub fn rename_target_references(old_id: &str, new_id: &str) -> Result<usize> {
163 rename_session_reference("target_template_id", old_id, new_id)
164}
165
166pub(super) fn rename_session_reference(
167 column: &'static str,
168 old_id: &str,
169 new_id: &str,
170) -> Result<usize> {
171 ensure!(
172 matches!(column, "last_profile" | "target_template_id"),
173 "unsupported session reference column"
174 );
175 let old_id = old_id.to_owned();
176 let new_id = new_id.to_owned();
177 submit_database_write("rename_session_reference", move |_| {
178 rename_session_reference_at(&database_path(), column, &old_id, &new_id)
179 })
180}
181
182pub(super) fn rename_session_reference_at(
183 path: &Path,
184 column: &str,
185 old_id: &str,
186 new_id: &str,
187) -> Result<usize> {
188 let mut connection = open(path)?;
189 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
190 let changed = tx.execute(
191 &format!("UPDATE sessions SET {column} = ?2 WHERE {column} = ?1"),
192 params![old_id, new_id],
193 )?;
194 tx.commit()?;
195 Ok(changed)
196}
197
198pub fn set_session_archived(session_id: &str, archived: bool) -> Result<()> {
202 let session_id = session_id.to_owned();
203 submit_database_write("set_session_archived", move |_| {
204 set_session_archived_to(&database_path(), &session_id, archived)
205 })
206}
207
208pub fn mark_session_target_missing(
213 session_id: &str,
214 detail: &str,
215 updated_at: &str,
216) -> Result<Option<SessionState>> {
217 let session_id = session_id.to_owned();
218 let detail = detail.to_owned();
219 let updated_at = updated_at.to_owned();
220 submit_database_write("mark_session_target_missing", move |_| {
221 mark_session_target_missing_to(&database_path(), &session_id, &detail, &updated_at)
222 })
223}
224
225pub(super) fn mark_session_target_missing_to(
226 path: &Path,
227 session_id: &str,
228 detail: &str,
229 updated_at: &str,
230) -> Result<Option<SessionState>> {
231 mark_session_target_missing_if_current_to(path, session_id, detail, updated_at, None)
232}
233
234pub fn mark_session_target_missing_if_current(
237 session_id: &str,
238 detail: &str,
239 updated_at: &str,
240 observed_updated_at: &str,
241) -> Result<Option<SessionState>> {
242 let session_id = session_id.to_owned();
243 let detail = detail.to_owned();
244 let updated_at = updated_at.to_owned();
245 let observed_updated_at = observed_updated_at.to_owned();
246 submit_database_write("mark_session_target_missing_if_current", move |_| {
247 mark_session_target_missing_if_current_to(
248 &database_path(),
249 &session_id,
250 &detail,
251 &updated_at,
252 Some(&observed_updated_at),
253 )
254 })
255}
256
257pub(super) fn mark_session_target_missing_if_current_to(
258 path: &Path,
259 session_id: &str,
260 detail: &str,
261 updated_at: &str,
262 observed_updated_at: Option<&str>,
263) -> Result<Option<SessionState>> {
264 let mut connection = open(path)?;
265 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
266 let changed = tx.execute(
267 "UPDATE sessions
268 SET state = CASE
269 WHEN EXISTS(
270 SELECT 1 FROM session_checkpoints
271 WHERE session_checkpoints.session_id = sessions.session_id
272 ) THEN 'error'
273 ELSE 'lost'
274 END,
275 last_error = ?2,
276 updated_at = ?3
277 WHERE session_id = ?1
278 AND (?4 IS NULL OR updated_at = ?4)
279 AND state IN ('provisioning', 'running', 'disconnected', 'error')",
280 params![session_id, detail, updated_at, observed_updated_at],
281 )?;
282 ensure!(changed <= 1, "updated {changed} sessions for {session_id}");
283 let state = if changed == 1 {
284 let stored: String = tx.query_row(
285 "SELECT state FROM sessions WHERE session_id = ?1",
286 [session_id],
287 |row| row.get(0),
288 )?;
289 Some(stored_session_state(&stored))
290 } else {
291 None
292 };
293 tx.commit()?;
294 Ok(state)
295}
296
297pub(super) fn set_session_archived_to(path: &Path, session_id: &str, archived: bool) -> Result<()> {
298 let connection = open(path)?;
299 let changed = connection.execute(
300 "UPDATE sessions SET archived = ?2 WHERE session_id = ?1",
301 params![session_id, archived],
302 )?;
303 if changed != 1 {
304 bail!("unknown session {session_id}");
305 }
306 Ok(())
307}
308
309pub fn hidden_native_sessions() -> Result<BTreeSet<(mj_core::config::HarnessKind, String)>> {
312 hidden_native_sessions_from(&database_path())
313}
314
315pub(super) fn hidden_native_sessions_from(
316 path: &Path,
317) -> Result<BTreeSet<(mj_core::config::HarnessKind, String)>> {
318 let connection = open_reader(path)?;
319 let mut statement =
320 connection.prepare("SELECT harness_kind, native_session_id FROM hidden_native_sessions")?;
321 let rows = statement.query_map([], |row| {
322 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
323 })?;
324 let mut hidden = BTreeSet::new();
325 for row in rows {
326 let (harness, native_session_id) = row?;
327 match harness.parse::<mj_core::config::HarnessKind>() {
330 Ok(harness) => {
331 hidden.insert((harness, native_session_id));
332 }
333 Err(_) => tracing::warn!(
334 harness = %harness,
335 "ignoring a hidden native session for a harness that is no longer supported"
336 ),
337 }
338 }
339 Ok(hidden)
340}
341
342pub fn set_native_session_hidden(
344 harness: mj_core::config::HarnessKind,
345 native_session_id: &str,
346 hidden: bool,
347) -> Result<()> {
348 let native_session_id = native_session_id.to_owned();
349 submit_database_write("set_native_session_hidden", move |_| {
350 set_native_session_hidden_to(&database_path(), harness, &native_session_id, hidden)
351 })
352}
353
354pub(super) fn set_native_session_hidden_to(
355 path: &Path,
356 harness: mj_core::config::HarnessKind,
357 native_session_id: &str,
358 hidden: bool,
359) -> Result<()> {
360 if native_session_id.trim().is_empty() {
361 bail!("native session id is empty");
362 }
363 let connection = open(path)?;
364 if hidden {
365 connection.execute(
366 "INSERT INTO hidden_native_sessions(harness_kind, native_session_id, hidden_at)
367 VALUES (?1, ?2, ?3)
368 ON CONFLICT(harness_kind, native_session_id) DO NOTHING",
369 params![harness.id(), native_session_id, Utc::now().to_rfc3339()],
370 )?;
371 } else {
372 connection.execute(
373 "DELETE FROM hidden_native_sessions
374 WHERE harness_kind = ?1 AND native_session_id = ?2",
375 params![harness.id(), native_session_id],
376 )?;
377 }
378 Ok(())
379}
380
381pub fn set_session_container_settings(
385 session_id: &str,
386 cpus: Option<&str>,
387 memory: Option<&str>,
388 mounts: &[AdditionalMount],
389 updated_at: &str,
390) -> Result<()> {
391 let session_id = session_id.to_owned();
392 let cpus = cpus.map(str::to_owned);
393 let memory = memory.map(str::to_owned);
394 let mounts = mounts.to_vec();
395 let updated_at = updated_at.to_owned();
396 submit_database_write("set_session_container_settings", move |_| {
397 set_session_container_settings_to(
398 &database_path(),
399 &session_id,
400 cpus.as_deref(),
401 memory.as_deref(),
402 &mounts,
403 &updated_at,
404 )
405 })
406}
407
408pub(super) fn set_session_container_settings_to(
409 path: &Path,
410 session_id: &str,
411 cpus: Option<&str>,
412 memory: Option<&str>,
413 mounts: &[AdditionalMount],
414 updated_at: &str,
415) -> Result<()> {
416 if updated_at.trim().is_empty() {
417 bail!("session update timestamp is empty");
418 }
419 crate::targets::validate_additional_mounts(mounts)?;
420 let mut connection = open(path)?;
421 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
422 let changed = tx.execute(
423 "UPDATE sessions
424 SET container_cpus = ?2, container_memory = ?3, updated_at = ?4
425 WHERE session_id = ?1",
426 params![session_id, cpus, memory, updated_at],
427 )?;
428 if changed != 1 {
429 bail!("unknown session {session_id}");
430 }
431 replace_mounts(&tx, session_id, mounts)?;
432 tx.commit()?;
433 Ok(())
434}
435
436pub(super) fn set_session_title_override_to(
437 path: &Path,
438 session_id: &str,
439 title: &str,
440 updated_at: &str,
441) -> Result<()> {
442 if title.trim().is_empty() {
443 bail!("session title is empty");
444 }
445 if updated_at.trim().is_empty() {
446 bail!("session update timestamp is empty");
447 }
448 let connection = open(path)?;
449 let changed = connection.execute(
450 "UPDATE sessions
451 SET session_title_override = ?2, updated_at = ?3
452 WHERE session_id = ?1",
453 params![session_id, title, updated_at],
454 )?;
455 if changed != 1 {
456 bail!("unknown session {session_id}");
457 }
458 Ok(())
459}
460
461pub fn set_session_acp_title(session_id: &str, title: Option<&str>) -> Result<()> {
464 let session_id = session_id.to_owned();
465 let title = title.map(str::to_owned);
466 submit_database_write("set_session_acp_title", move |_| {
467 set_session_acp_title_to(&database_path(), &session_id, title.as_deref())
468 })
469}
470
471pub(super) fn set_session_acp_title_to(
472 path: &Path,
473 session_id: &str,
474 title: Option<&str>,
475) -> Result<()> {
476 if title.is_some_and(|title| title.trim().is_empty()) {
477 bail!("ACP session title is empty");
478 }
479 let title = title.and_then(mj_core::state::normalize_session_title);
480 let connection = open(path)?;
481 let changed = connection.execute(
482 "UPDATE sessions SET acp_session_title = ?2 WHERE session_id = ?1",
483 params![session_id, title],
484 )?;
485 if changed != 1 {
486 bail!("unknown session {session_id}");
487 }
488 Ok(())
489}
490
491pub fn mark_session_worker_connected(
494 session_id: &str,
495 native_session_id: Option<&str>,
496 updated_at: &str,
497) -> Result<()> {
498 let session_id = session_id.to_owned();
499 let native_session_id = native_session_id.map(str::to_owned);
500 let updated_at = updated_at.to_owned();
501 submit_database_write("mark_session_worker_connected", move |_| {
502 mark_session_worker_connected_to(
503 &database_path(),
504 &session_id,
505 native_session_id.as_deref(),
506 &updated_at,
507 )
508 })
509}
510
511pub fn adopt_native_session_id(session_id: &str, native_session_id: &str) -> Result<()> {
515 let session_id = session_id.to_owned();
516 let native_session_id = native_session_id.to_owned();
517 submit_database_write("adopt_native_session_id", move |_| {
518 adopt_native_session_id_to(&database_path(), &session_id, &native_session_id)
519 })
520}
521
522pub(super) fn adopt_native_session_id_to(
523 path: &Path,
524 session_id: &str,
525 native_session_id: &str,
526) -> Result<()> {
527 let connection = open(path)?;
528 let changed = connection.execute(
529 "UPDATE sessions SET native_session_id = ?2 WHERE session_id = ?1",
530 params![session_id, native_session_id],
531 )?;
532 if changed != 1 {
533 bail!("unknown session {session_id}");
534 }
535 Ok(())
536}
537
538pub(super) fn mark_session_worker_connected_to(
539 path: &Path,
540 session_id: &str,
541 native_session_id: Option<&str>,
542 updated_at: &str,
543) -> Result<()> {
544 if updated_at.trim().is_empty() {
545 bail!("worker connection timestamp is empty");
546 }
547 let connection = open(path)?;
548 let changed = connection.execute(
549 "UPDATE sessions
550 SET state = 'running',
551 native_session_id = coalesce(?2, native_session_id),
552 updated_at = ?3,
553 last_error = NULL
554 WHERE session_id = ?1",
555 params![session_id, native_session_id, updated_at],
556 )?;
557 if changed != 1 {
558 bail!("unknown session {session_id}");
559 }
560 Ok(())
561}
562
563pub(super) fn recover_interrupted_checkpointing_sessions_to(
564 path: &Path,
565 updated_at: &str,
566) -> Result<usize> {
567 if updated_at.trim().is_empty() {
568 bail!("checkpoint recovery timestamp is empty");
569 }
570 let connection = open(path)?;
571 connection
572 .execute(
573 "UPDATE sessions
574 SET state = 'running', updated_at = ?1, last_checkpoint_error = ?2
575 WHERE state = 'checkpointing'",
576 params![
577 updated_at,
578 "checkpointing was interrupted by a controller restart; the target was left running"
579 ],
580 )
581 .context("recover interrupted checkpointing sessions")
582}
583
584pub(super) fn save_session_to(path: &Path, session: &SessionRecord) -> Result<()> {
585 save_session_with_container_size_to(path, session, None)
586}
587
588pub(super) fn save_session_with_container_size_to(
589 path: &Path,
590 session: &SessionRecord,
591 container_size: Option<(&str, HostContainerSize)>,
592) -> Result<()> {
593 validate_session_record(session)?;
594
595 let mut connection = open(path)?;
596 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
597 if let Some(existing_bundle) = tx
598 .query_row(
599 "SELECT bundle_id FROM session_contexts WHERE session_id = ?1",
600 [session.id.as_str()],
601 |row| row.get::<_, String>(0),
602 )
603 .optional()?
604 && existing_bundle != session.bundle_id
605 {
606 bail!(
607 "session {} was already associated with bundle {}, not {}",
608 session.id,
609 existing_bundle,
610 session.bundle_id
611 );
612 }
613 let mut session = session.clone();
614 let moving: bool = tx.query_row(
615 "SELECT EXISTS(SELECT 1 FROM session_moves WHERE session_id=?1
616 AND json_extract(operation_json, '$.phase') IN ('preparing','closing_source','resuming_destination','starting_queue'))",
617 [&session.id], |row| row.get(0),
618 )?;
619 if moving {
620 let (draft, title, acp_title, viewed, archived) = tx.query_row(
624 "SELECT draft_input, session_title_override, acp_session_title, viewed_through_event_ordinal, archived
625 FROM sessions WHERE session_id=?1", [&session.id], |row| Ok((
626 row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?, row.get::<_, Option<String>>(2)?,
627 row.get::<_, u64>(3)?, row.get::<_, bool>(4)?,
628 )),
629 )?;
630 session.draft_input = draft;
631 session.session_title_override = title;
632 session.acp_session_title = acp_title;
633 session.viewed_through_event_ordinal = viewed;
634 session.archived = archived;
635 }
636 insert_session(&tx, &session)?;
637 if let Some((host, size)) = container_size {
638 write_host_container_size(&tx, host, size)?;
639 }
640 tx.commit()?;
641 Ok(())
642}
643
644pub(super) fn save_lifecycle_session_to(path: &Path, session: &SessionRecord) -> Result<()> {
645 validate_session_record(session)?;
646
647 let mut connection = open(path)?;
648 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
649 update_lifecycle_fields(&tx, session)?;
650 tx.commit()?;
651 Ok(())
652}
653
654pub(super) fn save_checkpointed_session_to(path: &Path, session: &SessionRecord) -> Result<()> {
655 validate_session_record(session)?;
656
657 let mut connection = open(path)?;
658 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
659 update_lifecycle_fields(&tx, session)?;
660 tx.execute(
661 "UPDATE sessions SET native_session_id = ?2 WHERE session_id = ?1",
662 params![session.id, session.native_session_id],
663 )?;
664 replace_checkpoint(&tx, session)?;
665 tx.commit()?;
666 Ok(())
667}
668
669pub(super) fn validate_session_record(session: &SessionRecord) -> Result<()> {
670 let mut validation = State::default();
671 validation
672 .sessions
673 .insert(session.id.clone(), session.clone());
674 validation.validate()
675}
676
677pub fn delete_session(session_id: &str) -> Result<()> {
680 let session_id = session_id.to_owned();
681 submit_database_write("delete_session", move |_| {
682 delete_session_from(&database_path(), &session_id)
683 })
684}
685
686pub(super) fn delete_session_from(path: &Path, session_id: &str) -> Result<()> {
687 let connection = open(path)?;
688 connection.execute("DELETE FROM sessions WHERE session_id = ?1", [session_id])?;
689 Ok(())
690}
691
692pub fn set_session_draft_input(session_id: &str, draft: &str) -> Result<()> {
696 let session_id = session_id.to_owned();
697 let draft = draft.to_owned();
698 submit_database_write("set_session_draft_input", move |_| {
699 set_session_draft_input_at(&database_path(), &session_id, &draft)
700 })
701}
702
703pub(super) fn set_session_draft_input_at(path: &Path, session_id: &str, draft: &str) -> Result<()> {
704 let mut connection = open(path)?;
705 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
706 let updated = tx.execute(
707 "UPDATE sessions SET draft_input = ?2 WHERE session_id = ?1",
708 params![session_id, draft],
709 )?;
710 ensure!(updated == 1, "unknown session {session_id}");
711 tx.commit()?;
712 Ok(())
713}
714
715pub fn clear_session_draft_input_if_matches(session_id: &str, expected: &str) -> Result<()> {
717 let session_id = session_id.to_owned();
718 let expected = expected.to_owned();
719 submit_database_write("clear_session_draft_input_if_matches", move |connection| {
720 connection.execute(
721 "UPDATE sessions SET draft_input = '' WHERE session_id = ?1 AND draft_input = ?2",
722 params![session_id, expected],
723 )?;
724 Ok(())
725 })
726}
727
728pub fn record_recovery_success(
729 session_id: &str,
730 native_session_id: &str,
731 checkpoint: &CheckpointMetadata,
732) -> Result<()> {
733 let session_id = session_id.to_owned();
734 let native_session_id = native_session_id.to_owned();
735 let checkpoint = checkpoint.clone();
736 submit_database_write("record_recovery_success", move |_| {
737 record_recovery_success_to(
738 &database_path(),
739 &session_id,
740 &native_session_id,
741 &checkpoint,
742 )
743 })
744}
745
746pub(super) fn record_recovery_success_to(
747 path: &Path,
748 session_id: &str,
749 native_session_id: &str,
750 checkpoint: &CheckpointMetadata,
751) -> Result<()> {
752 let mut connection = open(path)?;
753 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
754 let changed = tx.execute(
755 "UPDATE sessions
756 SET native_session_id = ?2, last_checkpoint_error = NULL
757 WHERE session_id = ?1",
758 params![session_id, native_session_id],
759 )?;
760 if changed != 1 {
761 bail!("unknown session {session_id}");
762 }
763 tx.execute(
764 "INSERT INTO session_checkpoints(
765 session_id, archive_path, sha256, created_at, event_frontier
766 ) VALUES (?1,?2,?3,?4,?5)
767 ON CONFLICT(session_id) DO UPDATE SET
768 archive_path = excluded.archive_path,
769 sha256 = excluded.sha256,
770 created_at = excluded.created_at,
771 event_frontier = excluded.event_frontier",
772 params![
773 session_id,
774 path_to_blob(&checkpoint.archive_path),
775 checkpoint.sha256,
776 checkpoint.created_at,
777 checkpoint.event_frontier,
778 ],
779 )?;
780 tx.commit()?;
781 Ok(())
782}
783
784pub fn record_recovery_failure(session_id: &str, detail: &str) -> Result<()> {
785 let session_id = session_id.to_owned();
786 let detail = detail.to_owned();
787 submit_database_write("record_recovery_failure", move |_| {
788 record_recovery_failure_to(&database_path(), &session_id, &detail)
789 })
790}
791
792pub(super) fn record_recovery_failure_to(
793 path: &Path,
794 session_id: &str,
795 detail: &str,
796) -> Result<()> {
797 let connection = open(path)?;
798 let changed = connection.execute(
799 "UPDATE sessions SET last_checkpoint_error = ?2 WHERE session_id = ?1",
800 params![session_id, detail],
801 )?;
802 if changed != 1 {
803 bail!("unknown session {session_id}");
804 }
805 Ok(())
806}
807
808pub fn rebind_session_bundle(session_id: &str, bundle_id: &str) -> Result<()> {
815 let session_id = session_id.to_owned();
816 let bundle_id = bundle_id.to_owned();
817 submit_database_write("rebind_session_bundle", move |_| {
818 rebind_session_bundle_to(&database_path(), &session_id, &bundle_id)
819 })
820}
821
822pub(super) fn rebind_session_bundle_to(
823 path: &Path,
824 session_id: &str,
825 bundle_id: &str,
826) -> Result<()> {
827 let mut connection = open(path)?;
828 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
829 let changed = tx.execute(
830 "UPDATE session_contexts SET bundle_id = ?2 WHERE session_id = ?1",
831 params![session_id, bundle_id],
832 )?;
833 if changed == 0 {
834 tx.execute(
835 "INSERT INTO session_contexts(session_id, bundle_id, created_at) VALUES (?1, ?2, ?3)",
836 params![session_id, bundle_id, Utc::now().to_rfc3339()],
837 )?;
838 }
839 tx.commit()?;
840 Ok(())
841}