1use std::collections::HashMap;
41
42use serde_json::Value;
43use uuid::Uuid;
44
45use khive_storage::note::Note;
46use khive_storage::types::SqlValue;
47use khive_storage::{SqlStatement, StorageCapability, StorageError};
48use khive_types::SubstrateKind;
49
50use crate::atomic_plan::{AddNotePlan, AffectedRowGuard, PlanStatement, PostCommitEffect};
51use crate::atomic_runner::{AtomicOpPlan, AtomicRunOutcome};
52use crate::config::NamespaceToken;
53use crate::curation::note_fts_document;
54use crate::error::{RuntimeError, RuntimeResult};
55use crate::runtime::KhiveRuntime;
56
57pub struct AtomicNoteSpec<'a> {
63 pub token: &'a NamespaceToken,
65 pub id: Option<Uuid>,
70 pub kind: &'a str,
71 pub name: Option<&'a str>,
72 pub content: &'a str,
73 pub properties: Option<Value>,
74}
75
76#[derive(Clone, Copy, Default)]
77pub(crate) struct AtomicNoteOptions<'a> {
78 pub salience: Option<f64>,
79 pub decay_factor: Option<f64>,
80 pub embedding_model: Option<&'a str>,
81 pub embedding_content: Option<&'a str>,
82 pub embed: Option<bool>,
83 pub key: Option<&'a str>,
84 pub memory_visibility_receipt: bool,
87 pub replay_receipt: bool,
92 pub fence: Option<&'a crate::note_write::NoteFences>,
93 pub properties_already_derived: bool,
103}
104
105pub(crate) struct AtomicNoteRequest<'a> {
106 pub spec: AtomicNoteSpec<'a>,
107 pub options: AtomicNoteOptions<'a>,
108}
109
110pub(crate) struct PreparedAtomicNotes {
111 pub notes: Vec<Note>,
112 pub plans: Vec<AtomicOpPlan>,
113 pub embedding_truncation: crate::retrieval::EmbeddingTruncationReport,
114}
115
116pub(crate) fn non_finite_index(data: &[f32]) -> Option<usize> {
121 data.iter().position(|v| !v.is_finite())
122}
123
124pub(crate) fn non_finite_vector_error(idx: usize, value: f32) -> RuntimeError {
129 RuntimeError::Storage(StorageError::InvalidInput {
130 capability: StorageCapability::Vectors,
131 operation: "vec_insert".into(),
132 message: format!(
133 "non-finite value at index {idx}: {value} \
134 (NaN/Inf values corrupt distance computations)"
135 ),
136 })
137}
138
139#[cfg(any(test, feature = "fault-injection"))]
152fn injected_failure_statement(label: &str) -> PlanStatement {
153 PlanStatement {
154 statement: SqlStatement {
155 sql: "UPDATE notes SET updated_at = updated_at WHERE 1 = 0".to_string(),
156 params: vec![],
157 label: Some(label.to_string()),
158 },
159 guard: Some(AffectedRowGuard::exactly(1)),
160 }
161}
162
163#[cfg(any(test, feature = "fault-injection"))]
164fn maybe_inject_fts_failure(namespace: &str, label: &str) -> Option<PlanStatement> {
165 crate::operations::consume_fts_fail_fault(namespace).then(|| injected_failure_statement(label))
166}
167#[cfg(not(any(test, feature = "fault-injection")))]
168fn maybe_inject_fts_failure(_namespace: &str, _label: &str) -> Option<PlanStatement> {
169 None
170}
171
172#[cfg(any(test, feature = "fault-injection"))]
173fn maybe_inject_vector_failure(namespace: &str, label: &str) -> Option<PlanStatement> {
174 crate::operations::consume_vector_fail_fault(namespace)
175 .then(|| injected_failure_statement(label))
176}
177#[cfg(not(any(test, feature = "fault-injection")))]
178fn maybe_inject_vector_failure(_namespace: &str, _label: &str) -> Option<PlanStatement> {
179 None
180}
181
182#[allow(clippy::too_many_arguments)]
192pub(crate) fn vector_insert_statements(
193 table: &str,
194 namespace: &str,
195 subject_id: Uuid,
196 field: &str,
197 embedding_model: &str,
198 embedding: &[f32],
199 label_prefix: &str,
200) -> Vec<PlanStatement> {
201 vector_insert_statements_for_substrate(
202 table,
203 namespace,
204 subject_id,
205 SubstrateKind::Note,
206 field,
207 embedding_model,
208 embedding,
209 label_prefix,
210 None,
211 )
212}
213
214#[allow(clippy::too_many_arguments)]
217pub(crate) fn vector_insert_statements_for_substrate(
218 table: &str,
219 namespace: &str,
220 subject_id: Uuid,
221 substrate: SubstrateKind,
222 field: &str,
223 embedding_model: &str,
224 embedding: &[f32],
225 label_prefix: &str,
226 insert_guard: Option<AffectedRowGuard>,
227) -> Vec<PlanStatement> {
228 let subject = subject_id.to_string();
229 let model_key = table
230 .strip_prefix("vec_")
231 .expect("runtime vector tables use the vec_ prefix");
232 let kind_str = substrate.to_string();
233 let blob = khive_storage::encode_f32_native(embedding);
234 vec![
235 PlanStatement {
238 statement: SqlStatement {
239 sql: format!(
240 "INSERT INTO ann_write_log \
241 (namespace, embedding_model, kind, field, subject_id, op) \
242 SELECT namespace, embedding_model, kind, field, subject_id, 'delete' \
243 FROM {table} WHERE subject_id = ?1 AND NOT \
244 (namespace = ?2 AND embedding_model = ?3 AND kind = ?4 AND field = ?5)"
245 ),
246 params: vec![
247 SqlValue::Text(subject.clone()),
248 SqlValue::Text(namespace.to_string()),
249 SqlValue::Text(embedding_model.to_string()),
250 SqlValue::Text(kind_str.clone()),
251 SqlValue::Text(field.to_string()),
252 ],
253 label: Some(format!("{label_prefix}-log-delete")),
254 },
255 guard: None,
256 },
257 PlanStatement {
258 statement: SqlStatement {
259 sql: format!("DELETE FROM {table} WHERE subject_id = ?1"),
260 params: vec![SqlValue::Text(subject.clone())],
261 label: Some(format!("{label_prefix}-delete")),
262 },
263 guard: None,
264 },
265 PlanStatement {
268 statement: SqlStatement {
269 sql: "DELETE FROM vector_provenance \
270 WHERE model_key = ?1 AND subject_id = ?2"
271 .to_string(),
272 params: vec![
273 SqlValue::Text(model_key.to_string()),
274 SqlValue::Text(subject.clone()),
275 ],
276 label: Some(format!("{label_prefix}-clear-provenance")),
277 },
278 guard: None,
279 },
280 PlanStatement {
281 statement: SqlStatement {
282 sql: format!(
283 "INSERT INTO {table} \
284 (subject_id, namespace, kind, field, embedding_model, embedding) \
285 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
286 ),
287 params: vec![
288 SqlValue::Text(subject.clone()),
289 SqlValue::Text(namespace.to_string()),
290 SqlValue::Text(kind_str.clone()),
291 SqlValue::Text(field.to_string()),
292 SqlValue::Text(embedding_model.to_string()),
293 SqlValue::Blob(blob),
294 ],
295 label: Some(format!("{label_prefix}-insert")),
296 },
297 guard: insert_guard,
298 },
299 PlanStatement {
300 statement: SqlStatement {
301 sql: "INSERT INTO ann_write_log \
302 (namespace, embedding_model, kind, field, subject_id, op) \
303 VALUES (?1, ?2, ?3, ?4, ?5, 'upsert')"
304 .to_string(),
305 params: vec![
306 SqlValue::Text(namespace.to_string()),
307 SqlValue::Text(embedding_model.to_string()),
308 SqlValue::Text(kind_str),
309 SqlValue::Text(field.to_string()),
310 SqlValue::Text(subject),
311 ],
312 label: Some(format!("{label_prefix}-log-upsert")),
313 },
314 guard: insert_guard,
315 },
316 ]
317}
318
319#[cfg(test)]
326pub(crate) async fn create_notes_atomic(
327 runtime: &KhiveRuntime,
328 specs: Vec<AtomicNoteSpec<'_>>,
329) -> RuntimeResult<Vec<Note>> {
330 Ok(create_notes_atomic_with_report(runtime, specs).await?.0)
331}
332
333pub async fn create_notes_atomic_with_report(
338 runtime: &KhiveRuntime,
339 specs: Vec<AtomicNoteSpec<'_>>,
340) -> RuntimeResult<(Vec<Note>, crate::retrieval::EmbeddingTruncationReport)> {
341 create_notes_atomic_with_attachments(runtime, specs, &[]).await
342}
343
344pub async fn create_notes_atomic_with_attachments(
347 runtime: &KhiveRuntime,
348 specs: Vec<AtomicNoteSpec<'_>>,
349 attachments: &[khive_storage::NewAttachment],
350) -> RuntimeResult<(Vec<Note>, crate::retrieval::EmbeddingTruncationReport)> {
351 validate_note_attachments(runtime, attachments)?;
352 let mut prepared = prepare_atomic_notes(runtime, specs, AtomicNoteOptions::default()).await?;
353 append_note_attachments(&mut prepared, attachments)?;
354 match crate::atomic_runner::run_atomic_unit_with_note_versions(
355 runtime.sql().as_ref(),
356 prepared.plans,
357 true,
358 )
359 .await
360 {
361 Ok((AtomicRunOutcome::Committed { .. }, versions)) => {
362 assert_eq!(
363 prepared.notes.len(),
364 versions.len(),
365 "one revision receipt per prepared note"
366 );
367 for (note, version) in prepared.notes.iter_mut().zip(versions) {
368 note.version = version;
369 }
370 Ok((prepared.notes, prepared.embedding_truncation))
371 }
372 Ok((
373 AtomicRunOutcome::RolledBack {
374 failed_op_index,
375 failure,
376 },
377 _,
378 )) => Err(RuntimeError::Internal(format!(
379 "atomic multi-note write rolled back at op {failed_op_index}: {failure:?}"
380 ))),
381 Err(e) => Err(RuntimeError::Storage(e.0)),
382 }
383}
384
385pub(crate) fn validate_note_attachments(
386 runtime: &KhiveRuntime,
387 attachments: &[khive_storage::NewAttachment],
388) -> RuntimeResult<()> {
389 if !attachments.is_empty() {
390 runtime.attachments()?;
393 }
394 for attachment in attachments {
395 attachment.validate()?;
396 }
397 Ok(())
398}
399
400pub(crate) fn append_note_attachments(
401 prepared: &mut PreparedAtomicNotes,
402 attachments: &[khive_storage::NewAttachment],
403) -> RuntimeResult<()> {
404 for (plan, note) in prepared.plans.iter_mut().zip(&prepared.notes) {
405 let AtomicOpPlan::AddNote(plan) = plan else {
406 return Err(RuntimeError::Internal(
407 "expected prepared attachment owner note".into(),
408 ));
409 };
410 for attachment in attachments {
411 let row = khive_storage::Attachment::from_new(
412 note.id,
413 khive_storage::AttachmentSubstrate::Note,
414 attachment.clone(),
415 note.created_at,
416 );
417 plan.statements.push(PlanStatement {
420 statement: khive_db::stores::attachment::attachment_upsert_statement(&row)?,
421 guard: Some(AffectedRowGuard::exactly(1)),
422 });
423 }
424 }
425 Ok(())
426}
427
428pub(crate) async fn prepare_atomic_notes(
429 runtime: &KhiveRuntime,
430 specs: Vec<AtomicNoteSpec<'_>>,
431 options: AtomicNoteOptions<'_>,
432) -> RuntimeResult<PreparedAtomicNotes> {
433 validate_atomic_note_options(&options)?;
434 if options.embed != Some(false) {
435 if let Some(model) = options.embedding_model {
436 runtime.resolve_embedding_model(Some(model))?;
437 }
438 }
439 prepare_atomic_note_requests(
440 runtime,
441 specs
442 .into_iter()
443 .map(|spec| AtomicNoteRequest { spec, options })
444 .collect(),
445 )
446 .await
447}
448
449fn validate_atomic_note_options(options: &AtomicNoteOptions<'_>) -> RuntimeResult<()> {
450 if let Some(value) = options.salience {
451 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
452 return Err(RuntimeError::InvalidInput(
453 "salience must be a finite value in [0.0, 1.0]".into(),
454 ));
455 }
456 }
457 if let Some(value) = options.decay_factor {
458 if !value.is_finite() || value < 0.0 {
459 return Err(RuntimeError::InvalidInput(
460 "decay_factor must be a finite value >= 0.0".into(),
461 ));
462 }
463 }
464 Ok(())
465}
466
467pub(crate) async fn prepare_atomic_note_requests(
468 runtime: &KhiveRuntime,
469 requests: Vec<AtomicNoteRequest<'_>>,
470) -> RuntimeResult<PreparedAtomicNotes> {
471 let mut embed_model_names = Vec::new();
472 let mut note_models = Vec::with_capacity(requests.len());
473 for request in &requests {
474 let options = &request.options;
475 validate_atomic_note_options(options)?;
476 let models = if options.embed == Some(false) {
477 Vec::new()
478 } else if let Some(model) = options.embedding_model {
479 runtime.resolve_embedding_model(Some(model))?;
480 vec![model.to_owned()]
481 } else {
482 runtime.embedding_models_for_note_kind(request.spec.kind)
483 };
484 let mut indices = Vec::with_capacity(models.len());
485 for model in models {
486 let index = match embed_model_names.iter().position(|name| name == &model) {
487 Some(index) => index,
488 None => {
489 embed_model_names.push(model);
490 embed_model_names.len() - 1
491 }
492 };
493 indices.push(index);
494 }
495 note_models.push(indices);
496 }
497 let mut notes: Vec<Note> = Vec::with_capacity(requests.len());
500 for (index, request) in requests.iter().enumerate() {
501 let spec = &request.spec;
502 let options = &request.options;
503 let record = format!("note[{index}]");
504 runtime.validate_note_kind(spec.kind)?;
505 let properties = if options.properties_already_derived {
514 spec.properties.clone()
515 } else {
516 runtime.derive_note_write_properties(spec.kind, spec.token, spec.properties.clone())?
517 };
518 crate::secret_gate::reject_reserved_secret_gate_property(properties.as_ref())?;
519 crate::secret_gate::check_note_fields_at(
520 spec.content,
521 spec.name,
522 properties.as_ref(),
523 &record,
524 )?;
525
526 let ns = spec.token.namespace().as_str();
527 let mut note = Note::new(ns, spec.kind, spec.content);
528 note.key = options.key.map(str::to_owned);
529 if let Some(salience) = options.salience {
530 note = note.with_salience(salience);
531 }
532 if let Some(decay_factor) = options.decay_factor {
533 note = note.with_decay(decay_factor);
534 }
535 if let Some(id) = spec.id {
536 note.id = id;
537 }
538 if let Some(n) = spec.name {
539 note = note.with_name(n);
540 }
541 if let Some(p) = properties {
542 note = note.with_properties(p);
543 }
544 notes.push(note);
545 }
546
547 let memory_receipts: Vec<bool> = notes
551 .iter()
552 .zip(&requests)
553 .map(|(note, request)| {
554 note.kind == "memory"
555 && (request.options.key.is_some() || request.options.memory_visibility_receipt)
556 })
557 .collect();
558 if memory_receipts.iter().any(|capture| *capture) {
559 runtime.require_visibility_cutover()?;
560 }
561
562 let mut content_group_by_text: HashMap<&str, usize> = HashMap::new();
566 let mut content_groups: Vec<Vec<usize>> = Vec::new();
567 let mut note_content_groups: Vec<usize> = Vec::with_capacity(notes.len());
568 for (note_idx, note) in notes.iter().enumerate() {
569 let text = requests[note_idx]
570 .options
571 .embedding_content
572 .unwrap_or_else(|| crate::curation::note_embedding_text_ref(note));
573 let content_group_idx = match content_group_by_text.get(text) {
574 Some(&idx) => {
575 content_groups[idx].push(note_idx);
576 idx
577 }
578 None => {
579 let idx = content_groups.len();
580 content_group_by_text.insert(text, idx);
581 content_groups.push(vec![note_idx]);
582 idx
583 }
584 };
585 note_content_groups.push(content_group_idx);
586 }
587
588 let mut embedding_outcomes: Vec<Vec<Option<crate::retrieval::DocumentEmbeddingOutcome>>> =
590 content_groups
591 .iter()
592 .map(|_| vec![None; embed_model_names.len()])
593 .collect();
594 let mut embedding_truncation = crate::retrieval::EmbeddingTruncationReport::default();
595
596 if !embed_model_names.is_empty() {
601 let models = embed_model_names
605 .iter()
606 .map(|name| {
607 runtime
608 .vector_model_metadata(name)
609 .map(|(name, dimensions)| (crate::config::sanitize_key(&name), dimensions))
610 })
611 .collect::<RuntimeResult<Vec<_>>>()?;
612 let model_specs: Vec<_> = models
613 .iter()
614 .map(|(key, dimensions)| (key.as_str(), *dimensions))
615 .collect();
616 runtime.backend().ensure_vector_tables(&model_specs)?;
617
618 let usage_ctx = crate::usage::current();
619 let mut join_set = tokio::task::JoinSet::new();
620 for (model_idx, model_name) in embed_model_names.iter().enumerate() {
621 let mut groups = Vec::new();
622 let mut texts = Vec::new();
623 let mut token = None;
624 for (group_idx, note_indices) in content_groups.iter().enumerate() {
625 let Some(¬e_idx) = note_indices
626 .iter()
627 .find(|&&index| note_models[index].contains(&model_idx))
628 else {
629 continue;
630 };
631 token.get_or_insert_with(|| requests[note_idx].spec.token.clone());
632 groups.push(group_idx);
633 texts.push(
634 requests[note_idx]
635 .options
636 .embedding_content
637 .unwrap_or_else(|| {
638 crate::curation::note_embedding_text_ref(¬es[note_idx])
639 })
640 .to_owned(),
641 );
642 }
643 let rt = runtime.clone();
644 let token = token.expect("a selected model has at least one note");
645 let name = model_name.clone();
646 let ctx = usage_ctx.clone();
647 join_set.spawn(crate::runtime::inherit_request_embedder_scope(async move {
648 let fut = async {
649 let mut outcomes = Vec::with_capacity(texts.len());
650 for chunk in texts.chunks(lattice_embed::DEFAULT_MAX_BATCH_SIZE) {
651 outcomes.extend(
652 rt.embed_document_batch_with_model_outcomes_for_token(
653 &token, &name, chunk,
654 )
655 .await?,
656 );
657 }
658 Ok::<_, RuntimeError>(outcomes)
659 };
660 let result = match ctx {
661 Some(ctx) => crate::usage::scope(ctx, fut).await,
662 None => fut.await,
663 };
664 (groups, model_idx, result)
665 }));
666 }
667
668 while let Some(joined) = join_set.join_next().await {
669 match joined {
670 Ok((groups, model_idx, Ok(outcomes))) => {
671 for (group_idx, outcome) in groups.into_iter().zip(outcomes) {
672 embedding_outcomes[group_idx][model_idx] = Some(outcome);
673 }
674 }
675 Ok((_, _, Err(e))) => {
676 join_set.abort_all();
677 return Err(e);
678 }
679 Err(join_err) => {
680 join_set.abort_all();
681 return Err(RuntimeError::Internal(format!(
682 "embed task panicked: {join_err}"
683 )));
684 }
685 }
686 }
687 }
688
689 for (note_idx, &content_group_idx) in note_content_groups.iter().enumerate() {
692 for &model_idx in ¬e_models[note_idx] {
693 let outcome = embedding_outcomes[content_group_idx][model_idx]
694 .as_ref()
695 .expect("every requested embedding was observed");
696 embedding_truncation.observe(outcome);
697 if outcome.truncated {
698 tracing::warn!(
699 model = %outcome.model_name,
700 source_bytes = outcome.source_bytes,
701 embedded_bytes = outcome.embedded_bytes,
702 "atomic note embedding input truncated; full content will be stored unchanged"
703 );
704 }
705 }
706 }
707
708 for outcomes_for_content in &embedding_outcomes {
714 for outcome in outcomes_for_content.iter().flatten() {
715 if let Some(idx) = non_finite_index(&outcome.vector) {
716 return Err(non_finite_vector_error(idx, outcome.vector[idx]));
717 }
718 }
719 }
720
721 let mut plans: Vec<AtomicOpPlan> = Vec::with_capacity(notes.len());
724 for (note_idx, note) in notes.iter().enumerate() {
725 let outcomes_for_note = &embedding_outcomes[note_content_groups[note_idx]];
726 let mut statements = vec![PlanStatement {
727 statement: if note.key.is_some() {
728 khive_db::stores::note::note_insert_keyed_statement(note)
729 } else {
730 khive_db::stores::note::note_upsert_statement(note)
731 },
732 guard: Some(AffectedRowGuard::exactly(1)),
733 }];
734
735 if memory_receipts[note_idx] {
736 statements.push(PlanStatement {
737 statement: SqlStatement {
738 sql:
739 "INSERT INTO memory_visibility_receipts (namespace, note_id, model_count) \
740 VALUES (?1, ?2, ?3)"
741 .into(),
742 params: vec![
743 SqlValue::Text(note.namespace.clone()),
744 SqlValue::Text(note.id.to_string()),
745 SqlValue::Integer(note_models[note_idx].len() as i64),
746 ],
747 label: Some("memory-visibility-receipt".into()),
748 },
749 guard: Some(AffectedRowGuard::exactly(1)),
750 });
751 }
752
753 if let Some(fault) = maybe_inject_fts_failure(¬e.namespace, "fault-injected-fts") {
754 statements.push(fault);
755 } else {
756 statements.push(PlanStatement {
766 statement: khive_db::stores::text::delete_document_statement(
767 "fts_notes",
768 ¬e.namespace,
769 note.id,
770 ),
771 guard: None,
772 });
773 for statement in khive_db::stores::text::insert_document_statements(
779 "fts_notes",
780 ¬e_fts_document(note),
781 ) {
782 statements.push(PlanStatement {
783 statement,
784 guard: None,
785 });
786 }
787 }
788
789 if let Some(fault) = maybe_inject_vector_failure(¬e.namespace, "fault-injected-vector") {
790 statements.push(fault);
791 } else {
792 for &model_idx in ¬e_models[note_idx] {
793 let model_name = &embed_model_names[model_idx];
794 let outcome = outcomes_for_note[model_idx]
795 .as_ref()
796 .expect("every model index observed exactly once");
797 let table = format!("vec_{}", crate::config::sanitize_key(model_name));
798 statements.extend(vector_insert_statements(
799 &table,
800 ¬e.namespace,
801 note.id,
802 "note.content",
803 model_name,
804 &outcome.vector,
805 &format!("atomic-message-vec-{table}-{}", note.id),
806 ));
807 if memory_receipts[note_idx] {
808 statements.push(PlanStatement {
813 statement: SqlStatement {
814 sql: "INSERT INTO memory_visibility_fences \
815 (namespace, note_id, model, ann_write_log_seq) \
816 VALUES (?1, ?2, ?3, last_insert_rowid())"
817 .into(),
818 params: vec![
819 SqlValue::Text(note.namespace.clone()),
820 SqlValue::Text(note.id.to_string()),
821 SqlValue::Text(model_name.clone()),
822 ],
823 label: Some("memory-visibility-fence".into()),
824 },
825 guard: Some(AffectedRowGuard::exactly(1)),
826 });
827 }
828 }
829 }
830
831 if memory_receipts[note_idx] {
834 statements.push(PlanStatement {
835 statement: SqlStatement {
836 sql: "INSERT INTO memory_visibility_epochs (note_id, namespace, epoch) \
837 VALUES (?1, ?2, 'modern')"
838 .into(),
839 params: vec![
840 SqlValue::Text(note.id.to_string()),
841 SqlValue::Text(note.namespace.clone()),
842 ],
843 label: Some("memory-visibility-epoch".into()),
844 },
845 guard: Some(AffectedRowGuard::exactly(1)),
846 });
847 }
848
849 plans.push(AtomicOpPlan::AddNote(Box::new(AddNotePlan {
850 note_guard: Some(crate::note_write::NoteWriteGuard {
851 namespace: requests[note_idx].spec.token.namespace().as_str().into(),
852 target_id: note.id,
853 expected_version: None,
854 fence: requests[note_idx].options.fence.cloned(),
855 create_key: note
856 .key
857 .as_ref()
858 .map(|key| crate::note_write::CreateKeyClaim {
859 kind: note.kind.clone(),
860 key: key.clone(),
861 content: note.content.clone(),
862 properties: note.properties.clone(),
863 replay_signal: requests[note_idx].options.replay_receipt,
864 }),
865 }),
866 note_id: note.id,
867 statements,
868 post_commit: PostCommitEffect::None,
869 })));
870 }
871
872 Ok(PreparedAtomicNotes {
873 notes,
874 plans,
875 embedding_truncation,
876 })
877}
878
879#[cfg(test)]
880#[path = "atomic_message_tests.rs"]
881mod tests;