cortex-ai-memory 2.3.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/Cargo.lock +6 -7
  2. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/PKG-INFO +1 -1
  3. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/Cargo.toml +2 -1
  4. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/export.rs +7 -0
  5. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/lib.rs +22 -5
  6. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/crypto.rs +78 -0
  7. cortex_ai_memory-2.4.0/cortex-core/src/sync/mod.rs +1211 -0
  8. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/oplog.rs +13 -4
  9. cortex_ai_memory-2.4.0/cortex-core/src/sync/snapshot.rs +717 -0
  10. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/state.rs +125 -0
  11. cortex_ai_memory-2.4.0/cortex-core/tests/test_reserved_namespace.rs +112 -0
  12. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_sync.rs +1 -1
  13. cortex_ai_memory-2.4.0/cortex-core/tests/test_sync_hardening.rs +554 -0
  14. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-python/Cargo.toml +2 -1
  15. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/pyproject.toml +1 -1
  16. cortex_ai_memory-2.3.0/cortex-core/src/sync/mod.rs +0 -687
  17. cortex_ai_memory-2.3.0/cortex-core/src/sync/snapshot.rs +0 -361
  18. cortex_ai_memory-2.3.0/cortex-core/tests/test_reserved_namespace.rs +0 -35
  19. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/Cargo.toml +0 -0
  20. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/README.md +0 -0
  21. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/benches/comparison_bench.rs +0 -0
  22. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/benches/memory_index_bench.rs +0 -0
  23. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/belief.rs +0 -0
  24. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/compression.rs +0 -0
  25. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/consolidation.rs +0 -0
  26. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/context.rs +0 -0
  27. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/embedder.rs +0 -0
  28. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/episode.rs +0 -0
  29. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/events.rs +0 -0
  30. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/inference.rs +0 -0
  31. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/people.rs +0 -0
  32. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/plugin.rs +0 -0
  33. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/plugin_manager.rs +0 -0
  34. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/plugins/mod.rs +0 -0
  35. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/plugins/tag_classifier.rs +0 -0
  36. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/procedural.rs +0 -0
  37. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/relationship.rs +0 -0
  38. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/retrieval.rs +0 -0
  39. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/semantic.rs +0 -0
  40. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/memory_index.rs +0 -0
  41. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/mod.rs +0 -0
  42. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/sqlite.rs +0 -0
  43. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/traits.rs +0 -0
  44. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/hlc.rs +0 -0
  45. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/merge.rs +0 -0
  46. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/provider.rs +0 -0
  47. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/secret.rs +0 -0
  48. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/watcher.rs +0 -0
  49. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/types.rs +0 -0
  50. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/src/working.rs +0 -0
  51. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_belief.rs +0 -0
  52. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cache_and_perf.rs +0 -0
  53. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_compression.rs +0 -0
  54. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_compression_advanced.rs +0 -0
  55. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_consolidation.rs +0 -0
  56. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_consolidation_advanced.rs +0 -0
  57. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_context.rs +0 -0
  58. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_contradiction_flow.rs +0 -0
  59. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cortex_e2e.rs +0 -0
  60. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cortex_full.rs +0 -0
  61. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_episode.rs +0 -0
  62. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_events.rs +0 -0
  63. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_full_privacy_chain.rs +0 -0
  64. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_gdrive_real.rs +0 -0
  65. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_ingest_parity.rs +0 -0
  66. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_memory_index.rs +0 -0
  67. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_optimizations.rs +0 -0
  68. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_people.rs +0 -0
  69. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_plugin.rs +0 -0
  70. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_plugin_manager_ext.rs +0 -0
  71. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_privacy_sync.rs +0 -0
  72. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_procedural.rs +0 -0
  73. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_retrieval.rs +0 -0
  74. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_retrieval_advanced.rs +0 -0
  75. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_semantic.rs +0 -0
  76. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_storage.rs +0 -0
  77. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_storage_advanced.rs +0 -0
  78. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_temporal_retrieval.rs +0 -0
  79. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_traits_defaults.rs +0 -0
  80. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_types.rs +0 -0
  81. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_working_memory.rs +0 -0
  82. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-python/README.md +0 -0
  83. {cortex_ai_memory-2.3.0 → cortex_ai_memory-2.4.0}/cortex-python/src/lib.rs +0 -0
@@ -573,7 +573,7 @@ dependencies = [
573
573
 
574
574
  [[package]]
575
575
  name = "cortex-core"
576
- version = "2.3.0"
576
+ version = "2.4.0"
577
577
  dependencies = [
578
578
  "aes-gcm",
579
579
  "argon2",
@@ -606,7 +606,7 @@ dependencies = [
606
606
 
607
607
  [[package]]
608
608
  name = "cortex-http"
609
- version = "2.3.0"
609
+ version = "2.4.0"
610
610
  dependencies = [
611
611
  "axum",
612
612
  "chrono",
@@ -623,19 +623,19 @@ dependencies = [
623
623
 
624
624
  [[package]]
625
625
  name = "cortex-mcp-server"
626
- version = "2.3.0"
626
+ version = "2.4.0"
627
627
  dependencies = [
628
628
  "axum",
629
629
  "chrono",
630
630
  "clap",
631
631
  "cortex-core",
632
632
  "hostname",
633
+ "libc",
633
634
  "rand 0.8.5",
634
635
  "serde",
635
636
  "serde_json",
636
637
  "tokio",
637
638
  "tower",
638
- "tower-http",
639
639
  "tracing",
640
640
  "tracing-subscriber",
641
641
  "uuid",
@@ -643,7 +643,7 @@ dependencies = [
643
643
 
644
644
  [[package]]
645
645
  name = "cortex-python"
646
- version = "2.3.0"
646
+ version = "2.4.0"
647
647
  dependencies = [
648
648
  "cortex-core",
649
649
  "pyo3",
@@ -652,7 +652,7 @@ dependencies = [
652
652
 
653
653
  [[package]]
654
654
  name = "cortex-wasm"
655
- version = "2.3.0"
655
+ version = "2.4.0"
656
656
  dependencies = [
657
657
  "chrono",
658
658
  "js-sys",
@@ -3507,7 +3507,6 @@ dependencies = [
3507
3507
  "http-body",
3508
3508
  "iri-string",
3509
3509
  "pin-project-lite",
3510
- "tokio",
3511
3510
  "tower",
3512
3511
  "tower-layer",
3513
3512
  "tower-service",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortex-ai-memory
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,7 +1,8 @@
1
1
  [package]
2
2
  name = "cortex-core"
3
- version = "2.3.0"
3
+ version = "2.4.0"
4
4
  edition = "2021"
5
+ rust-version = "1.89"
5
6
 
6
7
  [features]
7
8
  default = ["encrypted-db"]
@@ -117,6 +117,13 @@ pub fn import_all(
117
117
  };
118
118
 
119
119
  if let Some(memories) = data.memories {
120
+ // The Muse export is consent given on this device. Import never recreates it:
121
+ // a crafted file could otherwise plant rows Muse would serve. After a restore, the
122
+ // user re-exports with `gateway allow`.
123
+ let memories: Vec<_> = memories
124
+ .into_iter()
125
+ .filter(|m| m.namespace.as_deref() != Some(crate::types::MUSE_EXPORT_NAMESPACE))
126
+ .collect();
120
127
  report.memories = memories.len();
121
128
  for mem in &memories {
122
129
  storage.store_memory(mem)?;
@@ -559,15 +559,14 @@ impl Cortex {
559
559
  Ok(())
560
560
  }
561
561
 
562
- /// Auto-generate embedding if embedder is available and none provided.
563
- /// Lazily initializes the embedding model on first call.
564
- #[allow(unused_variables)]
565
562
  /// Embed `text` with the local model, or `None` when embeddings are compiled out,
566
563
  /// disabled via `CORTEX_NO_EMBEDDINGS`, or the model failed to load.
567
564
  pub fn embed_query(&self, text: &str) -> Option<Vec<f32>> {
568
565
  self.auto_embed(text, None)
569
566
  }
570
567
 
568
+ /// Auto-generate embedding if embedder is available and none provided.
569
+ /// Lazily initializes the embedding model on first call.
571
570
  fn auto_embed(&self, text: &str, embedding: Option<Vec<f32>>) -> Option<Vec<f32>> {
572
571
  #[cfg(not(feature = "embeddings"))]
573
572
  let _ = text;
@@ -818,8 +817,19 @@ impl Cortex {
818
817
  return None;
819
818
  }
820
819
  let emb = embedding.as_ref()?;
821
- if let Some((existing_id, sim)) = self.index.search(emb, 1).first().copied() {
822
- if sim >= self.dedup_config.near_dedup_threshold {
820
+ // A Muse export copy is not the user's memory: merging into it would drop the new
821
+ // ingest (and a later `gateway revoke` would lose the only copy). Skip export copies
822
+ // and keep looking at the next-nearest neighbours instead of giving up.
823
+ const NEAR_DEDUP_CANDIDATES: usize = 8;
824
+ let is_export_copy = |id: Uuid| {
825
+ matches!(self.storage.get_memory(id),
826
+ Ok(Some(m)) if m.namespace.as_deref() == Some(crate::types::MUSE_EXPORT_NAMESPACE))
827
+ };
828
+ for (existing_id, sim) in self.index.search(emb, NEAR_DEDUP_CANDIDATES) {
829
+ if sim < self.dedup_config.near_dedup_threshold {
830
+ break; // results are ordered by similarity
831
+ }
832
+ if !is_export_copy(existing_id) {
823
833
  return Some(NearDup::Existing(existing_id));
824
834
  }
825
835
  }
@@ -1456,6 +1466,13 @@ impl Cortex {
1456
1466
  .storage
1457
1467
  .get_memory(id)?
1458
1468
  .ok_or_else(|| CortexError::NotFound(format!("memory {id}")))?;
1469
+ // Muse export copies are local-only consent records; they must never reach sync.
1470
+ if privacy.is_syncable() && mem.namespace.as_deref() == Some(crate::types::MUSE_EXPORT_NAMESPACE) {
1471
+ return Err(CortexError::InvalidInput(format!(
1472
+ "memories in the reserved '{}' namespace cannot be made syncable",
1473
+ crate::types::MUSE_EXPORT_NAMESPACE
1474
+ )));
1475
+ }
1459
1476
  let was_syncable = mem.privacy.is_syncable();
1460
1477
  mem.privacy = privacy;
1461
1478
  self.storage.update_memory(&mem)?;
@@ -113,6 +113,25 @@ pub struct EncryptionManifest {
113
113
  pub hmac_salt: Option<String>, // base64-encoded salt for HMAC key derivation
114
114
  #[serde(skip_serializing_if = "Option::is_none")]
115
115
  pub hmac: Option<String>, // base64-encoded HMAC of manifest content (computed without hmac and hmac_salt fields)
116
+ /// The snapshot new devices must bootstrap from. Covered by the manifest HMAC, so an
117
+ /// attacker without the passphrase can neither replay an older snapshot (same name, older
118
+ /// content) nor make restore fall back to one. Omitted when `None`, which keeps the HMAC
119
+ /// input of manifests written before this field existed byte-identical.
120
+ #[serde(default, skip_serializing_if = "Option::is_none")]
121
+ pub latest_snapshot: Option<SnapshotPointer>,
122
+ /// Monotonic counter bumped on every manifest write (first creation = 1). Covered by the
123
+ /// HMAC; devices remember the highest value they accepted and reject lower ones, so a
124
+ /// replayed older manifest (with its older snapshot pointer or key version) is detected.
125
+ /// Absent in legacy manifests (treated as 0); omitted when `None` for HMAC compatibility.
126
+ #[serde(default, skip_serializing_if = "Option::is_none")]
127
+ pub generation: Option<u64>,
128
+ }
129
+
130
+ /// Names the authoritative encrypted snapshot: its file name and its `HMAC:` line value.
131
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132
+ pub struct SnapshotPointer {
133
+ pub file: String,
134
+ pub mac: String,
116
135
  }
117
136
 
118
137
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -140,6 +159,8 @@ pub fn new_encryption_manifest() -> EncryptionManifest {
140
159
  key_version: Some(0), // Start at version 0; higher versions for rotated keys
141
160
  hmac_salt: None, // Will be computed during sync initialization
142
161
  hmac: None, // HMAC will be computed during sync initialization
162
+ latest_snapshot: None,
163
+ generation: None,
143
164
  }
144
165
  }
145
166
 
@@ -285,6 +306,19 @@ pub fn decrypt_line(ctx: &CryptoContext, encrypted_line: &str) -> Result<Vec<u8>
285
306
  }
286
307
  }
287
308
 
309
+ /// Key version of an envelope: `ENC1` is version 0; `ENC2` carries it in its first two
310
+ /// payload bytes. `None` if the line is not a well-formed envelope header. Only the first
311
+ /// base64 quantum is decoded, so this is cheap even for a large snapshot envelope.
312
+ pub fn envelope_version(line: &str) -> Option<u32> {
313
+ if line.starts_with(ENC_PREFIX) {
314
+ return Some(0);
315
+ }
316
+ let head = line.strip_prefix(ENC2_PREFIX)?.get(..4)?;
317
+ let bytes = base64::engine::general_purpose::STANDARD.decode(head).ok()?;
318
+ let version = bytes.get(..VERSION_LEN)?;
319
+ Some(u16::from_le_bytes([version[0], version[1]]) as u32)
320
+ }
321
+
288
322
  /// Check if a line is encrypted (either envelope version).
289
323
  pub fn is_encrypted_line(line: &str) -> bool {
290
324
  line.starts_with(ENC_PREFIX) || line.starts_with(ENC2_PREFIX)
@@ -588,4 +622,48 @@ mod tests {
588
622
  assert!(is_encrypted_line("ENC1:abc123"));
589
623
  assert!(!is_encrypted_line("{\"op_id\":\"...\"}"));
590
624
  }
625
+ #[test]
626
+ fn test_envelope_version() {
627
+ let m = test_manifest();
628
+ let ctx0 = derive_key("pass", &m).unwrap();
629
+ let ctx3 = derive_key("pass", &manifest_at_version(&m, 3)).unwrap();
630
+ assert_eq!(envelope_version(&encrypt_line(&ctx0, b"x").unwrap()), Some(0));
631
+ assert_eq!(envelope_version(&encrypt_line(&ctx3, b"x").unwrap()), Some(3));
632
+ assert_eq!(envelope_version("ENC2:"), None);
633
+ assert_eq!(envelope_version("ENC2:!!!!"), None);
634
+ assert_eq!(envelope_version("{\"op\":1}"), None);
635
+ }
636
+
637
+ /// Adding `latest_snapshot` must not change the HMAC input of manifests that lack it:
638
+ /// a manifest written before the field existed still verifies after a parse/re-serialize.
639
+ #[test]
640
+ fn test_legacy_manifest_without_pointer_still_verifies() {
641
+ let mut legacy = new_encryption_manifest();
642
+ legacy.hmac = None;
643
+ legacy.hmac_salt = None;
644
+ // Legacy wire form: exactly the pre-pointer field set.
645
+ let legacy_bytes = format!(
646
+ r#"{{"algorithm":"{}","kdf":"{}","salt":"{}","kdf_params":{{"time_cost":{},"mem_cost":{},"parallelism":{}}},"key_version":0}}"#,
647
+ legacy.algorithm,
648
+ legacy.kdf,
649
+ legacy.salt,
650
+ legacy.kdf_params.time_cost,
651
+ legacy.kdf_params.mem_cost,
652
+ legacy.kdf_params.parallelism,
653
+ )
654
+ .into_bytes();
655
+ let (hmac, salt) = compute_manifest_hmac(&legacy_bytes, "pass").unwrap();
656
+
657
+ let parsed: EncryptionManifest = serde_json::from_slice(&legacy_bytes).unwrap();
658
+ assert!(parsed.latest_snapshot.is_none());
659
+ let reserialized = serde_json::to_vec(&parsed).unwrap();
660
+ assert_eq!(reserialized, legacy_bytes);
661
+ assert!(verify_manifest_integrity(&reserialized, &hmac, "pass", Some(&salt)).unwrap());
662
+
663
+ // With a pointer the bytes (and so the HMAC) change — the pointer is authenticated.
664
+ let mut pinned = parsed.clone();
665
+ pinned.latest_snapshot = Some(SnapshotPointer { file: "f".into(), mac: "m".into() });
666
+ let pinned_bytes = serde_json::to_vec(&pinned).unwrap();
667
+ assert!(!verify_manifest_integrity(&pinned_bytes, &hmac, "pass", Some(&salt)).unwrap());
668
+ }
591
669
  }