1use khive_storage::{SqlStatement, SqlValue, SqlWriter, StorageError};
4use khive_types::{Details, KhiveError};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use crate::{KhiveRuntime, NamespaceToken, RuntimeError, RuntimeResult};
9
10#[cfg(test)]
26pub(crate) mod race_seam {
27 use std::sync::Arc;
28 use tokio::sync::Barrier;
29
30 tokio::task_local! {
31 pub(crate) static AFTER_PREPARE_BARRIER: Arc<Barrier>;
32 }
33
34 pub(crate) async fn pause_after_prepare() {
39 if let Ok(barrier) = AFTER_PREPARE_BARRIER.try_with(Arc::clone) {
40 barrier.wait().await;
41 barrier.wait().await;
42 }
43 }
44}
45
46pub const MAX_NOTE_FENCES: usize = 100;
48
49#[derive(Clone, Debug, Serialize)]
50pub struct NoteFence {
51 pub key: String,
52 pub kind: String,
53 pub expected_version: Option<i64>,
55 #[serde(skip_serializing_if = "Option::is_none")]
61 pub live_until: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
67 pub id: Option<Uuid>,
68}
69
70impl<'de> Deserialize<'de> for NoteFence {
71 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
72 #[derive(Deserialize)]
73 #[serde(deny_unknown_fields)]
74 struct Fields {
75 key: String,
76 kind: String,
77 #[serde(alias = "version")]
78 expected_version: Option<i64>,
79 live_until: Option<String>,
80 id: Option<Uuid>,
81 }
82
83 let value = serde_json::Value::deserialize(deserializer)?;
84 if value.get("expected_version").is_none() && value.get("version").is_none() {
86 return Err(serde::de::Error::custom(
87 "fence requires expected_version (positive integer or null)",
88 ));
89 }
90 let fields = Fields::deserialize(value).map_err(|error| {
91 serde::de::Error::custom(format!(
92 "fence requires expected_version (positive integer or null): {error}"
93 ))
94 })?;
95 Ok(Self {
96 key: fields.key,
97 kind: fields.kind,
98 expected_version: fields.expected_version,
99 live_until: fields.live_until,
100 id: fields.id,
101 })
102 }
103}
104
105impl NoteFence {
106 pub fn validate(&self) -> RuntimeResult<()> {
107 crate::keyed_memory::validate_memory_key(&self.key)?;
108 if self.kind.is_empty() {
109 return Err(RuntimeError::InvalidInput(
110 "fence requires a note kind".into(),
111 ));
112 }
113 if self.expected_version.is_some_and(|version| version < 1) {
114 return Err(RuntimeError::InvalidInput(
115 "fence requires expected_version (positive integer or null)".into(),
116 ));
117 }
118 if self.live_until.is_some() && self.expected_version.is_none() {
119 return Err(RuntimeError::InvalidInput(
122 "fence live_until requires a positive expected_version".into(),
123 ));
124 }
125 if self.live_until.as_ref().is_some_and(|path| path.is_empty()) {
126 return Err(RuntimeError::InvalidInput(
127 "fence live_until requires a document path".into(),
128 ));
129 }
130 if self.id.is_some() && self.expected_version.is_none() {
131 return Err(RuntimeError::InvalidInput(
134 "fence id requires a positive expected_version".into(),
135 ));
136 }
137 Ok(())
138 }
139}
140
141#[derive(Clone, Debug, Serialize)]
143#[serde(untagged)]
144pub enum NoteFences {
145 One(NoteFence),
146 Many(Vec<NoteFence>),
147}
148
149impl From<NoteFence> for NoteFences {
150 fn from(fence: NoteFence) -> Self {
151 Self::One(fence)
152 }
153}
154
155impl NoteFences {
156 fn validate_count(count: usize) -> RuntimeResult<()> {
157 crate::note_validation::validate_fence_count(count, MAX_NOTE_FENCES, || {
158 format!(
159 "fence list admits at most {MAX_NOTE_FENCES} entries; this call sent {count}: each fence is a read taken while holding the writer"
160 )
161 })
162 }
163
164 pub fn entries(&self) -> &[NoteFence] {
165 match self {
166 Self::One(fence) => std::slice::from_ref(fence),
167 Self::Many(fences) => fences,
168 }
169 }
170
171 pub fn validate(&self) -> RuntimeResult<()> {
172 Self::validate_count(self.entries().len())?;
173 if self.entries().is_empty() {
174 return Err(RuntimeError::InvalidInput(
175 "fence list must not be empty".into(),
176 ));
177 }
178 let mut seen = std::collections::HashMap::new();
179 for (index, fence) in self.entries().iter().enumerate() {
180 fence.validate()?;
181 if let Some(first) = seen.insert((&fence.kind, &fence.key), index) {
182 return Err(RuntimeError::InvalidInput(format!(
183 "duplicate fence (kind, key) at indices {first} and {index}"
184 )));
185 }
186 }
187 Ok(())
188 }
189}
190
191impl<'de> Deserialize<'de> for NoteFences {
192 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
193 let value = serde_json::Value::deserialize(deserializer)?;
194 let fences = match value {
195 serde_json::Value::Object(_) => {
196 Self::One(NoteFence::deserialize(value).map_err(serde::de::Error::custom)?)
197 }
198 serde_json::Value::Array(entries) => {
199 Self::validate_count(entries.len()).map_err(serde::de::Error::custom)?;
200 let mut fences = Vec::with_capacity(entries.len());
201 for (index, entry) in entries.into_iter().enumerate() {
202 fences.push(NoteFence::deserialize(entry).map_err(|error| {
203 serde::de::Error::custom(format!("fence entry {index}: {error}"))
204 })?);
205 }
206 Self::Many(fences)
207 }
208 _ => {
209 return Err(serde::de::Error::custom(
210 "fence requires an object or a non-empty list of objects with key, kind and expected_version (positive integer or null)",
211 ));
212 }
213 };
214 fences.validate().map_err(serde::de::Error::custom)?;
215 Ok(fences)
216 }
217}
218
219pub fn deserialize_optional_fences<'de, D: serde::Deserializer<'de>>(
221 deserializer: D,
222) -> Result<Option<NoteFences>, D::Error> {
223 NoteFences::deserialize(deserializer).map(Some)
224}
225
226#[derive(Clone, Debug, Default)]
227pub struct NoteWriteOptions {
228 pub key: Option<String>,
229 pub expected_version: Option<i64>,
230 pub fence: Option<NoteFences>,
231 pub embed: Option<bool>,
232}
233
234impl NoteWriteOptions {
235 pub fn validate(&self) -> RuntimeResult<()> {
236 if let Some(key) = &self.key {
237 crate::keyed_memory::validate_memory_key(key)?;
238 }
239 if self.expected_version.is_some_and(|version| version < 1) {
240 return Err(RuntimeError::InvalidInput(
241 "expected_version must be positive".into(),
242 ));
243 }
244 if let Some(fence) = &self.fence {
245 fence.validate()?;
246 }
247 Ok(())
248 }
249}
250
251#[derive(Clone, Debug)]
257pub(crate) struct CreateKeyClaim {
258 pub kind: String,
259 pub key: String,
260 pub content: String,
261 pub properties: Option<serde_json::Value>,
262 pub replay_signal: bool,
267}
268
269#[derive(Clone, Debug)]
270pub(crate) struct NoteWriteGuard {
271 pub namespace: String,
272 pub target_id: Uuid,
273 pub expected_version: Option<i64>,
274 pub fence: Option<NoteFences>,
275 pub create_key: Option<CreateKeyClaim>,
276}
277
278#[derive(Clone, Debug)]
279pub(crate) struct NoteVectors {
280 namespace: String,
281 subject_id: Uuid,
282}
283
284impl NoteVectors {
285 pub(crate) fn new(namespace: String, subject_id: Uuid) -> Self {
286 Self {
287 namespace,
288 subject_id,
289 }
290 }
291
292 async fn tables(writer: &mut dyn SqlWriter) -> Result<Vec<String>, StorageError> {
293 let tables = writer
296 .query_all(
297 SqlStatement::new(
298 "SELECT name FROM pragma_table_list \
299 WHERE schema='main' AND type='virtual' AND name GLOB 'vec_*' ORDER BY name",
300 vec![],
301 )
302 .labelled("note-write-guard"),
303 )
304 .await?;
305 let mut names = Vec::with_capacity(tables.len());
306 for row in tables {
307 let Some(SqlValue::Text(table)) = row.get("name") else {
308 return Err(StorageError::Internal(
309 "invalid vector table catalog row".into(),
310 ));
311 };
312 if !table.strip_prefix("vec_").is_some_and(|key| {
313 !key.is_empty() && key.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_')
314 }) {
315 return Err(StorageError::Internal(
316 "invalid persisted vector table name".into(),
317 ));
318 }
319 names.push(table.clone());
320 }
321 Ok(names)
322 }
323
324 pub(crate) async fn has_rows(&self, writer: &mut dyn SqlWriter) -> Result<bool, StorageError> {
325 for table in Self::tables(writer).await? {
326 if writer
327 .query_scalar(
328 SqlStatement::new(
329 format!(
330 "SELECT 1 FROM main.{table} WHERE namespace=?1 AND subject_id=?2 LIMIT 1"
331 ),
332 vec![
333 SqlValue::Text(self.namespace.clone()),
334 SqlValue::Text(self.subject_id.to_string()),
335 ],
336 )
337 .labelled("note-write-guard"),
338 )
339 .await?
340 .is_some()
341 {
342 return Ok(true);
343 }
344 }
345 Ok(false)
346 }
347
348 pub(crate) async fn apply(&self, writer: &mut dyn SqlWriter) -> Result<(), StorageError> {
349 for table in Self::tables(writer).await? {
350 let scope = vec![
351 SqlValue::Text(self.namespace.clone()),
352 SqlValue::Text(self.subject_id.to_string()),
353 ];
354 writer
355 .execute(
356 SqlStatement::new(
357 format!(
358 "INSERT INTO ann_write_log (namespace,embedding_model,kind,field,subject_id,op) \
359 SELECT namespace,embedding_model,kind,field,subject_id,'delete' \
360 FROM main.{table} WHERE namespace=?1 AND subject_id=?2"),
361 scope.clone(),
362 )
363 .labelled("note-write-guard"),
364 )
365 .await?;
366 writer
367 .execute(
368 SqlStatement::new(
369 format!("DELETE FROM main.{table} WHERE namespace=?1 AND subject_id=?2"),
370 scope,
371 )
372 .labelled("note-write-guard"),
373 )
374 .await?;
375 let model_key = table.strip_prefix("vec_").ok_or_else(|| {
376 StorageError::Internal("invalid persisted vector table name".into())
377 })?;
378 writer
379 .execute(
380 SqlStatement::new(
381 "DELETE FROM vector_provenance \
382 WHERE model_key=?1 AND namespace=?2 AND subject_id=?3",
383 vec![
384 SqlValue::Text(model_key.to_string()),
385 SqlValue::Text(self.namespace.clone()),
386 SqlValue::Text(self.subject_id.to_string()),
387 ],
388 )
389 .labelled("note-write-guard"),
390 )
391 .await?;
392 }
393 Ok(())
394 }
395}
396
397#[derive(Clone, Debug)]
398pub(crate) struct NoteEmbeddingInheritance {
399 pub vectors: NoteVectors,
400 pub kind: String,
401}
402
403#[derive(Clone, Debug, PartialEq, Eq)]
404pub enum NoteWriteConflict {
405 Version {
406 expected: i64,
407 current: i64,
408 },
409 Fence {
410 key: String,
411 expected: Option<i64>,
412 current: Option<i64>,
413 index: Option<usize>,
414 },
415 Key {
416 key: String,
417 existing_id: String,
418 equal: Option<bool>,
429 },
430 FenceDeadline(Box<FenceDeadline>),
434 FenceIdentity(Box<FenceIdentity>),
438}
439
440#[derive(Clone, Debug, PartialEq, Eq)]
443pub struct FenceIdentity {
444 pub key: String,
445 pub kind: String,
446 pub version: i64,
447 pub index: Option<usize>,
448 pub evidence: Vec<(&'static str, String)>,
449}
450
451#[derive(Clone, Debug, PartialEq, Eq)]
454pub struct FenceDeadline {
455 pub key: String,
456 pub kind: String,
457 pub version: i64,
458 pub field: String,
459 pub index: Option<usize>,
460 pub reason: &'static str,
461 pub evidence: Vec<(&'static str, String)>,
462}
463
464impl NoteWriteConflict {
465 pub fn into_error(self) -> KhiveError {
466 self.into_error_at_member(None)
467 }
468
469 pub(crate) fn into_error_at_member(self, member: Option<usize>) -> KhiveError {
470 let (message, mut details) = match self {
471 Self::Version { expected, current } => (
472 "note version precondition failed",
473 vec![
474 ("reason", "version_conflict".into()),
475 ("expected_version", expected.to_string()),
476 ("current_version", current.to_string()),
477 ],
478 ),
479 Self::Fence {
480 key,
481 expected,
482 current,
483 index,
484 } => {
485 let mut fields = vec![
486 ("reason", "fence_conflict".into()),
487 ("key", key),
488 (
489 "expected_version",
490 expected.map_or_else(|| "absent".into(), |version| version.to_string()),
491 ),
492 ];
493 if let Some(current) = current {
494 fields.push(("current_version", current.to_string()));
495 }
496 if let Some(index) = index {
497 fields.push(("index", index.to_string()));
498 }
499 ("note fence precondition failed", fields)
500 }
501 Self::Key {
502 key,
503 existing_id,
504 equal,
505 } => {
506 let mut fields = vec![
507 ("reason", "key_conflict".into()),
508 ("key", key),
509 ("existing_id", existing_id),
510 ];
511 if let Some(equal) = equal {
512 fields.push(("equal", equal.to_string()));
513 }
514 ("a live note already holds this key", fields)
515 }
516 Self::FenceDeadline(deadline) => {
517 let FenceDeadline {
518 key,
519 kind,
520 version,
521 field,
522 index,
523 reason,
524 evidence,
525 } = *deadline;
526 let mut fields = vec![
527 ("reason", reason.into()),
528 ("key", key),
529 ("kind", kind),
530 ("version", version.to_string()),
531 ("field", field),
532 ];
533 if let Some(index) = index {
534 fields.push(("index", index.to_string()));
535 }
536 fields.extend(evidence);
537 ("note fence time precondition failed", fields)
538 }
539 Self::FenceIdentity(identity) => {
540 let FenceIdentity {
541 key,
542 kind,
543 version,
544 index,
545 evidence,
546 } = *identity;
547 let mut fields = vec![
548 ("reason", "identity_conflict".into()),
549 ("key", key),
550 ("kind", kind),
551 ("version", version.to_string()),
552 ];
553 fields.extend(evidence);
554 if let Some(index) = index {
555 fields.push(("index", index.to_string()));
556 }
557 ("note fence identity precondition failed", fields)
558 }
559 };
560 if let Some(member) = member {
561 details.push(("member", member.to_string()));
562 }
563 KhiveError::conflict(message).with_details(Details::new_owned(details))
564 }
565}
566
567impl NoteWriteGuard {
568 pub(crate) async fn check_fence(
569 &self,
570 writer: &mut dyn SqlWriter,
571 ) -> Result<Option<NoteWriteConflict>, StorageError> {
572 let Some(fences) = &self.fence else {
573 return Ok(None);
574 };
575 let mut now: Option<i64> = None;
579 for (index, fence) in fences.entries().iter().enumerate() {
580 let holder = crate::fence_identity::read_holder(
581 writer,
582 &self.namespace,
583 &fence.kind,
584 &fence.key,
585 "note-write-guard",
586 )
587 .await?;
588 if let (Some(asserted), Some(holder)) = (fence.id, holder.as_ref()) {
594 if asserted != holder.id {
595 return Ok(Some(NoteWriteConflict::FenceIdentity(Box::new(
596 FenceIdentity {
597 key: fence.key.clone(),
598 kind: fence.kind.clone(),
599 version: fence.expected_version.unwrap_or_default(),
602 index: matches!(fences, NoteFences::Many(_)).then_some(index),
603 evidence: crate::fence_identity::identity_evidence(asserted, holder.id),
604 },
605 ))));
606 }
607 }
608 let current = holder.map(|holder| holder.version);
609 if current != fence.expected_version {
610 return Ok(Some(NoteWriteConflict::Fence {
611 key: fence.key.clone(),
612 expected: fence.expected_version,
613 current,
614 index: matches!(fences, NoteFences::Many(_)).then_some(index),
615 }));
616 }
617 if let Some(field) = &fence.live_until {
618 let clock = match now {
619 Some(clock) => clock,
620 None => {
621 let clock =
622 crate::live_until::writer_clock(writer, "note-write-guard-clock")
623 .await?;
624 now = Some(clock);
625 clock
626 }
627 };
628 if let Some(refusal) = crate::live_until::evaluate(
629 writer,
630 &self.namespace,
631 &fence.kind,
632 &fence.key,
633 field,
634 clock,
635 "note-write-guard-live-until",
636 )
637 .await?
638 {
639 return Ok(Some(NoteWriteConflict::FenceDeadline(Box::new(
640 FenceDeadline {
641 key: fence.key.clone(),
642 kind: fence.kind.clone(),
643 version: fence.expected_version.unwrap_or_default(),
646 field: field.clone(),
647 index: matches!(fences, NoteFences::Many(_)).then_some(index),
648 reason: refusal.reason(),
649 evidence: refusal.details(),
650 },
651 ))));
652 }
653 }
654 }
655 Ok(None)
656 }
657
658 pub(crate) async fn classify_refusal(
659 &self,
660 writer: &mut dyn SqlWriter,
661 ) -> Result<Option<NoteWriteConflict>, StorageError> {
662 if let Some(claim) = &self.create_key {
663 let holder = writer
668 .query_row(
669 SqlStatement::new(
670 "SELECT id, content, properties FROM notes \
671 WHERE namespace=?1 AND kind=?2 AND key=?3 AND deleted_at IS NULL",
672 vec![
673 SqlValue::Text(self.namespace.clone()),
674 SqlValue::Text(claim.kind.clone()),
675 SqlValue::Text(claim.key.clone()),
676 ],
677 )
678 .labelled("note-write-guard"),
679 )
680 .await?;
681 if let Some(row) = holder {
682 let existing_id = match row.get("id") {
683 Some(SqlValue::Text(id)) => id.clone(),
684 _ => {
685 return Err(StorageError::Internal(
686 "invalid keyed note holder identity".into(),
687 ))
688 }
689 };
690 let existing_content = match row.get("content") {
691 Some(SqlValue::Text(content)) => content.clone(),
692 _ => {
693 return Err(StorageError::Internal(
694 "invalid keyed note holder content".into(),
695 ))
696 }
697 };
698 let existing_properties: Option<serde_json::Value> = match row.get("properties") {
699 Some(SqlValue::Text(text)) => {
700 Some(serde_json::from_str(text).map_err(|_| {
701 StorageError::Internal("invalid keyed note holder properties".into())
702 })?)
703 }
704 Some(SqlValue::Null) | None => None,
705 _ => {
706 return Err(StorageError::Internal(
707 "invalid keyed note holder properties".into(),
708 ))
709 }
710 };
711 let equal = claim.replay_signal.then(|| {
725 existing_content == claim.content && existing_properties == claim.properties
726 });
727 return Ok(Some(NoteWriteConflict::Key {
728 key: claim.key.clone(),
729 existing_id,
730 equal,
731 }));
732 }
733 }
734 if let Some(expected) = self.expected_version {
735 let current = writer
736 .query_scalar(
737 SqlStatement::new(
738 "SELECT version FROM notes WHERE id=?1 AND deleted_at IS NULL",
739 vec![SqlValue::Text(self.target_id.to_string())],
740 )
741 .labelled("note-write-guard"),
742 )
743 .await?;
744 if let Some(SqlValue::Integer(current)) = current {
745 if current != expected {
746 return Ok(Some(NoteWriteConflict::Version { expected, current }));
747 }
748 }
749 }
750 Ok(None)
751 }
752}
753
754async fn check_keyed_create_holder(
768 access: &dyn khive_storage::SqlAccess,
769 guard: NoteWriteGuard,
770) -> RuntimeResult<Option<NoteWriteConflict>> {
771 use crate::atomic_runner::{
772 run_prepared_atomic_unit, PreparedAtomicError, PreparedAtomicOp, PreparedAtomicOutcome,
773 };
774 let op: PreparedAtomicOp<(), NoteWriteConflict> = Box::new(move |writer| {
775 Box::pin(async move {
776 if let Some(conflict) = guard.check_fence(writer).await? {
777 return Err(PreparedAtomicError::Refused {
778 failure: conflict,
779 message: "keyed create: fence stale at holder check".into(),
780 });
781 }
782 if let Some(conflict) = guard.classify_refusal(writer).await? {
783 return Err(PreparedAtomicError::Refused {
784 failure: conflict,
785 message: "keyed create: live holder found at holder check".into(),
786 });
787 }
788 Ok(((), Vec::new()))
789 })
790 });
791 match run_prepared_atomic_unit(access, op).await? {
792 PreparedAtomicOutcome::Committed { .. } => Ok(None),
793 PreparedAtomicOutcome::RolledBack(conflict) => Ok(Some(conflict)),
794 }
795}
796
797pub(crate) fn validate_head(note: &khive_storage::note::Note) -> RuntimeResult<()> {
798 if note.kind != "head" {
799 return Ok(());
800 }
801 if note.name.is_some() {
802 return Err(RuntimeError::InvalidInput("head notes have no name".into()));
803 }
804 serde_json::from_str::<serde_json::Value>(¬e.content).map_err(|error| {
805 RuntimeError::InvalidInput(format!("head content must be JSON text: {error}"))
806 })?;
807 if let Some(tags) = note
808 .properties
809 .as_ref()
810 .and_then(|p| p.get("tags"))
811 .and_then(|t| t.as_array())
812 {
813 for tag in tags.iter().filter_map(|tag| tag.as_str()) {
814 if let Some(kind) = tag.strip_prefix("kind:") {
815 if kind.len() > 64 || kind.contains('\0') {
816 return Err(RuntimeError::InvalidInput(
817 "head document kind must be at most 64 bytes without U+0000".into(),
818 ));
819 }
820 }
821 }
822 }
823 Ok(())
824}
825
826impl KhiveRuntime {
827 pub async fn get_note_by_key(
828 &self,
829 token: &NamespaceToken,
830 key: &str,
831 kind: Option<&str>,
832 after_key: bool,
833 ) -> RuntimeResult<khive_storage::note::Note> {
834 self.get_note_by_key_in_scope(token, key, kind, after_key, None)
835 .await
836 }
837
838 pub async fn get_note_by_key_in_scope(
842 &self,
843 token: &NamespaceToken,
844 key: &str,
845 kind: Option<&str>,
846 after_key: bool,
847 mailbox: Option<&khive_storage::note::NoteMailboxScope>,
848 ) -> RuntimeResult<khive_storage::note::Note> {
849 crate::keyed_memory::validate_memory_key(key)?;
850 let mut matches = self
851 .notes(token)?
852 .get_live_notes_by_key(token.namespace().as_str(), key, kind)
853 .await?;
854 if let Some(scope) = mailbox {
855 matches.retain(|note| crate::MailboxView::scope_permits_message_note(scope, note));
856 }
857 match matches.len() {
858 0 => {
859 let error = KhiveError::not_found("note key", key);
860 Err(if after_key {
861 error.with_details(Details::new_owned([
862 ("reason", "after_key_missing".into()),
863 ("key", key.into()),
864 ]))
865 } else {
866 error
867 }
868 .into())
869 }
870 1 => Ok(matches.remove(0)),
871 _ => {
872 let kinds = matches
873 .iter()
874 .map(|note| note.kind.as_str())
875 .collect::<Vec<_>>()
876 .join(",");
877 Err(KhiveError::conflict("note key is ambiguous")
878 .with_details(Details::new_owned([
879 ("reason", "key_ambiguous".into()),
880 ("key", key.into()),
881 ("kinds", kinds),
882 ]))
883 .into())
884 }
885 }
886 }
887
888 pub(crate) async fn prepare_versioned_note_update(
889 &self,
890 token: &NamespaceToken,
891 snapshot: khive_storage::note::Note,
892 patch: crate::curation::NotePatch,
893 ) -> RuntimeResult<(khive_storage::note::Note, crate::atomic_plan::UpdatePlan)> {
894 use crate::atomic_plan::{AffectedRowGuard, PlanStatement, PostCommitEffect, UpdatePlan};
895 let options = patch.write_options.clone();
896 options.validate()?;
897 if options.key.is_some() {
898 return Err(RuntimeError::InvalidInput("key is immutable".into()));
899 }
900 if let Some(fences) = &options.fence {
901 for fence in fences.entries() {
902 self.validate_note_kind(&fence.kind)?;
903 }
904 }
905 let expected_updated_at = snapshot.updated_at;
906 let expected_deleted_at = snapshot.deleted_at;
907 let expected_snapshot_version = snapshot.version;
908 let (mut note, text_changed, changed) = self
909 .prepare_update_note_from_snapshot(token, snapshot, patch)
910 .await?;
911 validate_head(¬e)?;
912 if !changed && options.embed.is_none() && options.expected_version.is_none() {
919 let assertion = SqlStatement {
920 sql: "SELECT 1 FROM notes WHERE id=?1 AND updated_at=?2 AND deleted_at IS ?3 AND version=?4"
921 .into(),
922 params: vec![
923 SqlValue::Text(note.id.to_string()),
924 SqlValue::Integer(expected_updated_at),
925 expected_deleted_at
926 .map(SqlValue::Integer)
927 .unwrap_or(SqlValue::Null),
928 SqlValue::Integer(expected_snapshot_version),
929 ],
930 label: Some("note-noop-assertion".into()),
931 };
932 let plan = UpdatePlan {
933 graph_effects: Vec::new(),
934 target_id: note.id,
935 statements: vec![PlanStatement {
936 statement: assertion,
937 guard: Some(AffectedRowGuard::exactly(1)),
938 }],
939 post_commit: PostCommitEffect::None,
940 edge_natural_key: None,
941 idempotent_noop: true,
942 entity_guard: None,
943 note_guard: Some(NoteWriteGuard {
944 namespace: token.namespace().as_str().into(),
945 target_id: note.id,
946 expected_version: options.expected_version,
947 fence: options.fence,
948 create_key: None,
949 }),
950 note_vector_purge: None,
951 note_embedding_inheritance: None,
952 };
953 return Ok((note, plan));
954 }
955 if !changed {
956 let minimum_updated_at = note.updated_at.checked_add(1).ok_or_else(|| {
961 RuntimeError::Internal(format!(
962 "note {} updated_at is already at i64::MAX and cannot advance",
963 note.id
964 ))
965 })?;
966 note.updated_at = chrono::Utc::now()
967 .timestamp_micros()
968 .max(minimum_updated_at);
969 }
970 let next_version = note
971 .version
972 .checked_add(1)
973 .ok_or_else(|| RuntimeError::InvalidInput("note version exhausted".into()))?;
974 note.version = next_version;
975 let mut update = if self.stream_member_error(¬e).await?.is_some() {
976 khive_db::stores::note::note_metadata_replace_if_unchanged_statement(
977 ¬e,
978 expected_updated_at,
979 expected_deleted_at,
980 )
981 } else {
982 khive_db::stores::note::note_replace_if_unchanged_statement(
983 ¬e,
984 expected_updated_at,
985 expected_deleted_at,
986 )
987 };
988 update
992 .params
993 .push(SqlValue::Integer(expected_snapshot_version));
994 update
995 .sql
996 .push_str(&format!(" AND version = ?{}", update.params.len()));
997 if let Some(version) = options.expected_version {
998 update.params.push(SqlValue::Integer(version));
999 update
1000 .sql
1001 .push_str(&format!(" AND version = ?{}", update.params.len()));
1002 }
1003 let mut statements = vec![PlanStatement {
1004 statement: update,
1005 guard: Some(AffectedRowGuard::exactly(1)),
1006 }];
1007 if text_changed {
1008 for sql in khive_db::stores::text::delete_document_statements(
1009 "fts_notes",
1010 ¬e.namespace,
1011 note.id,
1012 )
1013 .into_iter()
1014 .chain(khive_db::stores::text::insert_document_statements(
1015 "fts_notes",
1016 &crate::curation::note_fts_document(¬e),
1017 )) {
1018 statements.push(PlanStatement {
1019 statement: sql,
1020 guard: None,
1021 });
1022 }
1023 }
1024 statements.extend(crate::atomic_prepare::event_append_statements(
1028 token,
1029 ¬e.namespace,
1030 "update",
1031 khive_types::EventKind::NoteUpdated,
1032 khive_types::SubstrateKind::Note,
1033 note.id,
1034 serde_json::json!({
1035 "id": note.id,
1036 "namespace": note.namespace,
1037 "version": note.version,
1038 "text_changed": text_changed,
1039 }),
1040 )?);
1041 let post_commit =
1044 if options.embed != Some(false) && (text_changed || options.embed == Some(true)) {
1045 PostCommitEffect::ReindexNote {
1046 note_id: note.id,
1047 version: note.version,
1048 }
1049 } else if text_changed || options.embed == Some(false) {
1050 PostCommitEffect::NoteChanged {
1051 note_id: note.id,
1052 kind: note.kind.clone(),
1053 }
1054 } else {
1055 PostCommitEffect::None
1056 };
1057 let plan = UpdatePlan {
1058 graph_effects: Vec::new(),
1059 target_id: note.id,
1060 statements,
1061 post_commit,
1062 edge_natural_key: None,
1063 idempotent_noop: false,
1064 entity_guard: None,
1065 note_guard: Some(NoteWriteGuard {
1066 namespace: token.namespace().as_str().into(),
1067 target_id: note.id,
1068 expected_version: options.expected_version,
1069 fence: options.fence,
1070 create_key: None,
1071 }),
1072 note_vector_purge: (options.embed == Some(false))
1073 .then(|| NoteVectors::new(note.namespace.clone(), note.id)),
1074 note_embedding_inheritance: (text_changed && options.embed.is_none()).then(|| {
1075 NoteEmbeddingInheritance {
1076 vectors: NoteVectors::new(note.namespace.clone(), note.id),
1077 kind: note.kind.clone(),
1078 }
1079 }),
1080 };
1081 Ok((note, plan))
1082 }
1083
1084 #[allow(clippy::too_many_arguments)]
1085 pub async fn create_note_with_options(
1086 &self,
1087 token: &NamespaceToken,
1088 kind: &str,
1089 name: Option<&str>,
1090 content: &str,
1091 embedding_content: Option<&str>,
1092 salience: Option<f64>,
1093 decay_factor: Option<f64>,
1094 properties: Option<serde_json::Value>,
1095 annotates: Vec<Uuid>,
1096 embedding_model: Option<&str>,
1097 options: NoteWriteOptions,
1098 ) -> RuntimeResult<(
1099 khive_storage::note::Note,
1100 crate::retrieval::EmbeddingTruncationReport,
1101 )> {
1102 self.create_note_with_options_resolving_annotations(
1103 token,
1104 kind,
1105 name,
1106 content,
1107 embedding_content,
1108 salience,
1109 decay_factor,
1110 properties,
1111 std::future::ready(Ok(annotates)),
1112 embedding_model,
1113 options,
1114 )
1115 .await
1116 }
1117
1118 #[allow(clippy::too_many_arguments)]
1122 pub async fn create_note_with_options_resolving_annotations<F>(
1123 &self,
1124 token: &NamespaceToken,
1125 kind: &str,
1126 name: Option<&str>,
1127 content: &str,
1128 embedding_content: Option<&str>,
1129 salience: Option<f64>,
1130 decay_factor: Option<f64>,
1131 properties: Option<serde_json::Value>,
1132 annotations: F,
1133 embedding_model: Option<&str>,
1134 options: NoteWriteOptions,
1135 ) -> RuntimeResult<(
1136 khive_storage::note::Note,
1137 crate::retrieval::EmbeddingTruncationReport,
1138 )>
1139 where
1140 F: std::future::Future<Output = RuntimeResult<Vec<Uuid>>> + Send,
1141 {
1142 use crate::atomic_message::{AtomicNoteOptions, AtomicNoteSpec};
1143 use crate::atomic_runner::{run_atomic_unit, AtomicOpFailure, AtomicRunOutcome};
1144 use crate::note_create::{prepare_note_create, KeyPublication};
1145 options.validate()?;
1146 if options.expected_version.is_some() {
1147 return Err(RuntimeError::InvalidInput(
1148 "expected_version applies only to update".into(),
1149 ));
1150 }
1151 if let Some(fences) = &options.fence {
1152 for fence in fences.entries() {
1153 self.validate_note_kind(&fence.kind)?;
1154 }
1155 }
1156 if let Some(prefix) = embedding_content {
1157 if prefix.is_empty() || prefix.len() >= content.len() || !content.starts_with(prefix) {
1158 return Err(RuntimeError::InvalidInput(
1159 "embedding_content must be a non-empty proper prefix of content".into(),
1160 ));
1161 }
1162 crate::secret_gate::check_at(prefix, "note", "embedding_content")?;
1163 }
1164 let mut candidate =
1165 khive_storage::note::Note::new(token.namespace().as_str(), kind, content);
1166 candidate.name = name.map(str::to_owned);
1167 candidate.properties = properties.clone();
1168 validate_head(&candidate)?;
1169
1170 let Some(key) = options.key.as_deref() else {
1175 let annotates = annotations.await?;
1176 let (mut prepared, _) = prepare_note_create(
1177 self,
1178 AtomicNoteSpec {
1179 token,
1180 id: None,
1181 kind,
1182 name,
1183 content,
1184 properties,
1185 },
1186 AtomicNoteOptions {
1187 salience,
1188 decay_factor,
1189 embedding_model,
1190 embedding_content,
1191 embed: Some(options.embed.unwrap_or(kind != "head")),
1192 key: None,
1193 memory_visibility_receipt: false,
1194 replay_receipt: true,
1195 fence: options.fence.as_ref(),
1196 properties_already_derived: false,
1197 },
1198 &annotates,
1199 KeyPublication::AtInsert,
1200 )
1201 .await?;
1202 let note = prepared.notes.remove(0);
1203 return match run_atomic_unit(self.sql().as_ref(), prepared.plans).await {
1204 Ok(AtomicRunOutcome::Committed { .. }) => {
1205 self.fire_note_mutation_hook(¬e.kind, note.id).await;
1206 Ok((note, prepared.embedding_truncation))
1207 }
1208 Ok(AtomicRunOutcome::RolledBack {
1209 failure: AtomicOpFailure::NoteConflict(conflict),
1210 ..
1211 }) => Err(conflict.into_error().into()),
1212 Ok(AtomicRunOutcome::RolledBack { failure, .. }) => Err(RuntimeError::Internal(
1213 format!("note creation rolled back: {failure:?}"),
1214 )),
1215 Err(error) => Err(RuntimeError::Storage(error.0)),
1216 };
1217 };
1218
1219 let derived_properties =
1229 self.derive_note_write_properties(kind, token, properties.clone())?;
1230 let namespace: String = token.namespace().as_str().into();
1231
1232 if let Some(conflict) = check_keyed_create_holder(
1238 self.sql().as_ref(),
1239 NoteWriteGuard {
1240 namespace: namespace.clone(),
1241 target_id: candidate.id,
1242 expected_version: None,
1243 fence: options.fence.clone(),
1244 create_key: Some(CreateKeyClaim {
1245 kind: kind.to_owned(),
1246 key: key.to_owned(),
1247 content: content.to_owned(),
1248 properties: derived_properties.clone(),
1249 replay_signal: true,
1250 }),
1251 },
1252 )
1253 .await?
1254 {
1255 return Err(conflict.into_error().into());
1256 }
1257
1258 let prep_result = async {
1265 let annotates = annotations.await?;
1266 prepare_note_create(
1267 self,
1268 AtomicNoteSpec {
1269 token,
1270 id: None,
1271 kind,
1272 name,
1273 content,
1274 properties: derived_properties.clone(),
1275 },
1276 AtomicNoteOptions {
1277 salience,
1278 decay_factor,
1279 embedding_model,
1280 embedding_content,
1281 embed: Some(options.embed.unwrap_or(kind != "head")),
1282 key: Some(key),
1283 memory_visibility_receipt: false,
1284 replay_receipt: true,
1285 fence: options.fence.as_ref(),
1286 properties_already_derived: true,
1287 },
1288 &annotates,
1289 KeyPublication::AtInsert,
1290 )
1291 .await
1292 }
1293 .await;
1294
1295 #[cfg(test)]
1301 race_seam::pause_after_prepare().await;
1302
1303 match prep_result {
1304 Ok((mut prepared, _)) => {
1305 let note = prepared.notes.remove(0);
1306 match run_atomic_unit(self.sql().as_ref(), prepared.plans).await {
1307 Ok(AtomicRunOutcome::Committed { .. }) => {
1308 self.fire_note_mutation_hook(¬e.kind, note.id).await;
1309 Ok((note, prepared.embedding_truncation))
1310 }
1311 Ok(AtomicRunOutcome::RolledBack {
1312 failure: AtomicOpFailure::NoteConflict(conflict),
1313 ..
1314 }) => Err(conflict.into_error().into()),
1315 Ok(AtomicRunOutcome::RolledBack { failure, .. }) => Err(
1316 RuntimeError::Internal(format!("note creation rolled back: {failure:?}")),
1317 ),
1318 Err(error) => Err(RuntimeError::Storage(error.0)),
1319 }
1320 }
1321 Err(prep_error) => {
1322 match check_keyed_create_holder(
1328 self.sql().as_ref(),
1329 NoteWriteGuard {
1330 namespace,
1331 target_id: candidate.id,
1332 expected_version: None,
1333 fence: options.fence.clone(),
1334 create_key: Some(CreateKeyClaim {
1335 kind: kind.to_owned(),
1336 key: key.to_owned(),
1337 content: content.to_owned(),
1338 properties: derived_properties,
1339 replay_signal: true,
1340 }),
1341 },
1342 )
1343 .await?
1344 {
1345 Some(conflict) => Err(conflict.into_error().into()),
1346 None => Err(prep_error),
1347 }
1348 }
1349 }
1350 }
1351}