ci: lint all targets, run interop suites for real, compile benches

- clippy --all-targets plus a clawhdf5-format feature matrix (parallel, lz4,
  zstd, pcodec, fast-checksum); fix the accumulated lint backlog in test,
  bench and feature-gated code (no behaviour changes).
- Install python3 + h5py/numpy/netCDF4/xarray in the CI container and set
  CLAWHDF5_REQUIRE_INTEROP=1, which makes a missing interop dependency a test
  failure. Every h5py/netCDF4 interop test used to skip silently in CI. Run
  the #[ignore]d writer_h5py_tests suite explicitly.
- cargo bench --no-run so benches can't rot; fix bench.rs and memory_bench.rs,
  which no longer compiled against the current strategy/consolidation APIs.
- Optional fuzz smoke run via CLAWHDF5_FUZZ_SECONDS.
- CHANGELOG and docs/known-issues.md updated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 05:36:22 -07:00
co-authored by Claude Fable 5.1
parent 706189c3ef
commit bbe1baa208
30 changed files with 588 additions and 405 deletions
+16 -3
View File
@@ -483,7 +483,7 @@ fn rayon_benches(c: &mut Criterion) {
use rayon::prelude::*;
let query_norm = vector_search::compute_norm(&query);
let num_cores = rayon::current_num_threads().max(1);
let chunk_size = (n + num_cores - 1) / num_cores;
let chunk_size = n.div_ceil(num_cores);
let mut results: Vec<(usize, f32)> = vectors
.par_chunks(chunk_size)
.enumerate()
@@ -537,7 +537,7 @@ fn rayon_benches(c: &mut Criterion) {
use rayon::prelude::*;
let query_norm = vector_search::compute_norm(&query);
let num_cores = rayon::current_num_threads().max(1);
let chunk_size = (n + num_cores - 1) / num_cores;
let chunk_size = n.div_ceil(num_cores);
let mut results: Vec<(usize, f32)> = vectors
.par_chunks(chunk_size)
.enumerate()
@@ -766,12 +766,22 @@ fn adaptive_benches(c: &mut Criterion) {
.map(|v| vector_search::compute_norm(v))
.collect();
let tombstones = vec![0u8; n];
let flat: Vec<f32> = vectors.iter().flatten().copied().collect();
c.bench_function("adaptive_search_10k", |b| {
let hw = HardwareCapabilities::detect();
let strat = strategy::auto_select_strategy(n, &hw);
b.iter(|| {
strategy::search_with_metrics(&query, &vectors, &norms, &tombstones, 10, strat, None)
strategy::search_with_metrics(
&query,
&vectors,
&flat,
&norms,
&tombstones,
10,
strat,
None,
)
});
});
@@ -781,6 +791,7 @@ fn adaptive_benches(c: &mut Criterion) {
strategy::search_with_metrics(
&query,
&vectors,
&flat,
&norms,
&tombstones,
10,
@@ -795,6 +806,7 @@ fn adaptive_benches(c: &mut Criterion) {
strategy::search_with_metrics(
&query,
&vectors,
&flat,
&norms,
&tombstones,
10,
@@ -809,6 +821,7 @@ fn adaptive_benches(c: &mut Criterion) {
strategy::search_with_metrics(
&query,
&vectors,
&flat,
&norms,
&tombstones,
10,
+19 -6
View File
@@ -1,6 +1,7 @@
use clawhdf5_agent::bm25::BM25Index;
use clawhdf5_agent::consolidation::{
ConsolidationConfig, ConsolidationEngine, ImportanceScorer, ImportanceWeights, MemorySource,
UntrustedSource,
};
use clawhdf5_agent::hybrid::{hybrid_search, rrf_hybrid_search};
use clawhdf5_agent::knowledge::KnowledgeCache;
@@ -285,7 +286,12 @@ fn consolidation_benches(c: &mut Criterion) {
for i in 0..n {
let embedding = make_vec(&mut rng, DIM);
let chunk = format!("memory record {i} with some content");
engine.add_memory(chunk, embedding, MemorySource::User, now + i as f64);
engine.add_memory(
chunk,
embedding,
UntrustedSource::User,
now + i as f64,
);
}
engine
},
@@ -307,9 +313,10 @@ fn consolidation_benches(c: &mut Criterion) {
for i in 0..50usize {
let embedding = make_vec(&mut rng, DIM);
let chunk = format!("existing record {i}");
engine.add_memory(chunk, embedding, MemorySource::User, now + i as f64);
engine.add_memory(chunk, embedding, UntrustedSource::User, now + i as f64);
}
let records = engine.records().to_vec();
let record_refs: Vec<&_> = records.iter().collect();
let weights = ImportanceWeights::default();
let query_embedding = make_vec(&mut rng, DIM);
let sample_text =
@@ -317,7 +324,7 @@ fn consolidation_benches(c: &mut Criterion) {
group.bench_function("bench_importance_scoring", |b| {
b.iter(|| {
let surprise = ImportanceScorer::score_surprise(&query_embedding, &records);
let surprise = ImportanceScorer::score_surprise(&query_embedding, &record_refs);
let correction = ImportanceScorer::score_correction(&MemorySource::Correction);
let length = ImportanceScorer::score_length(sample_text);
ImportanceScorer::score_combined(surprise, correction, length, &weights)
@@ -354,7 +361,7 @@ fn temporal_benches(c: &mut Criterion) {
// Insert benchmark: measure time to insert 10k timestamps one by one
group.bench_function("bench_temporal_insert_10k", |b| {
b.iter_batched(
|| TemporalIndex::new(),
TemporalIndex::new,
|mut idx| {
for i in 0..N {
// Shuffle insertion order slightly using a simple offset pattern
@@ -442,7 +449,8 @@ fn large_consolidation_benches(c: &mut Criterion) {
let mut group = c.benchmark_group("consolidation_large");
group.sample_size(10);
for (label, n) in [("10k", 10_000usize)] {
{
let (label, n) = ("10k", 10_000usize);
group.bench_with_input(
BenchmarkId::new("bench_consolidation_cycle", label),
&n,
@@ -459,7 +467,12 @@ fn large_consolidation_benches(c: &mut Criterion) {
for i in 0..n {
let embedding = make_vec(&mut rng, DIM);
let chunk = format!("memory record {i} with content");
engine.add_memory(chunk, embedding, MemorySource::User, now + i as f64);
engine.add_memory(
chunk,
embedding,
UntrustedSource::User,
now + i as f64,
);
}
engine
},
+14 -13
View File
@@ -563,7 +563,7 @@ mod tests {
#[test]
fn test_importance_scorer_surprise_identical() {
let emb = unit_vec(4, 0);
let existing = vec![MemoryRecord {
let existing = [MemoryRecord {
id: 0,
chunk: "existing".to_string(),
embedding: emb.clone(),
@@ -603,23 +603,20 @@ mod tests {
fn test_importance_scorer_length() {
assert!((ImportanceScorer::score_length("")).abs() < f32::EPSILON);
// 50 words → 0.5
let fifty_words = std::iter::repeat("word")
.take(50)
let fifty_words = std::iter::repeat_n("word", 50)
.collect::<Vec<_>>()
.join(" ");
let s50 = ImportanceScorer::score_length(&fifty_words);
assert!((s50 - 0.5).abs() < 1e-5, "expected 0.5, got {s50}");
// 100 words → 1.0
let hundred_words = std::iter::repeat("word")
.take(100)
let hundred_words = std::iter::repeat_n("word", 100)
.collect::<Vec<_>>()
.join(" ");
assert_eq!(ImportanceScorer::score_length(&hundred_words), 1.0);
// 200 words → still 1.0 (clamped)
let two_hundred = std::iter::repeat("word")
.take(200)
let two_hundred = std::iter::repeat_n("word", 200)
.collect::<Vec<_>>()
.join(" ");
assert_eq!(ImportanceScorer::score_length(&two_hundred), 1.0);
@@ -693,9 +690,11 @@ mod tests {
// ---------------------------------------------------------------------------
#[test]
fn test_consolidate_eviction_working() {
let mut cfg = ConsolidationConfig::default();
cfg.working_capacity = 3;
cfg.working_to_episodic_threshold = 2.0; // never promote in this test
let cfg = ConsolidationConfig {
working_capacity: 3,
working_to_episodic_threshold: 2.0, // never promote in this test
..Default::default()
};
let mut engine = ConsolidationEngine::new(cfg);
// Add 5 records; all have very low importance so none get promoted.
@@ -853,9 +852,11 @@ mod tests {
// ---------------------------------------------------------------------------
#[test]
fn test_consolidate_episodic_eviction() {
let mut cfg = ConsolidationConfig::default();
cfg.episodic_capacity = 3;
cfg.working_to_episodic_threshold = 2.0; // never auto-promote from Working
let cfg = ConsolidationConfig {
episodic_capacity: 3,
working_to_episodic_threshold: 2.0, // never auto-promote from Working
..Default::default()
};
let mut engine = ConsolidationEngine::new(cfg);
// Seed 5 records directly in Episodic.
+14 -8
View File
@@ -777,8 +777,10 @@ mod tests {
#[test]
fn test_tech_disabled() {
let mut config = ExtractorConfig::default();
config.extract_technology = false;
let config = ExtractorConfig {
extract_technology: false,
..Default::default()
};
let e = EntityExtractor::new(config);
let entities = e.extract("We use Rust and Docker.");
assert!(
@@ -847,8 +849,10 @@ mod tests {
#[test]
fn test_date_disabled() {
let mut config = ExtractorConfig::default();
config.extract_dates = false;
let config = ExtractorConfig {
extract_dates: false,
..Default::default()
};
let e = EntityExtractor::new(config);
let entities = e.extract("Released on 2024-03-19.");
assert!(
@@ -981,8 +985,10 @@ mod tests {
#[test]
fn test_confidence_filter() {
let mut config = ExtractorConfig::default();
config.min_confidence = 0.95;
let config = ExtractorConfig {
min_confidence: 0.95,
..Default::default()
};
let e = EntityExtractor::new(config);
// Only dates (0.95) and techs (0.9) should survive; 0.9 < 0.95 filters techs.
let entities = e.extract("We use Rust since 2024-01-01.");
@@ -1002,7 +1008,7 @@ mod tests {
fn test_batch_dedup() {
let e = default_extractor();
let texts = ["We use Rust.", "Rust is fast.", "Also Rust for safety."];
let entities = e.extract_batch(&texts.iter().map(|s| *s).collect::<Vec<_>>());
let entities = e.extract_batch(&texts);
let rust_count = entities.iter().filter(|x| x.text == "Rust").count();
assert_eq!(rust_count, 1, "Rust should appear exactly once after dedup");
}
@@ -1011,7 +1017,7 @@ mod tests {
fn test_batch_multiple_types() {
let e = default_extractor();
let texts = ["Deploy with Docker.", "We merged last week."];
let entities = e.extract_batch(&texts.iter().map(|s| *s).collect::<Vec<_>>());
let entities = e.extract_batch(&texts);
assert!(
entities
.iter()
+193 -193
View File
@@ -839,6 +839,199 @@ fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
impl HDF5Memory {
pub fn set_strategy(&mut self, s: Box<dyn MemoryStrategy>) {
self.strategy = Some(s);
}
pub fn record(&mut self, exchange: Exchange) -> Result<StrategyOutput> {
let strat = self.strategy.as_ref().ok_or_else(|| {
MemoryError::Schema(
"strategy not initialized: call set_strategy() before record()".to_owned(),
)
})?;
let view = memory_strategy::CacheStoreView::new(&self.cache, &self.knowledge);
let output = strat.evaluate(&exchange, &view);
for e in &output.entries {
self.cache.push(
e.chunk.clone(),
e.embedding.clone(),
e.source_channel.clone(),
e.timestamp,
e.session_id.clone(),
e.tags.clone(),
);
}
for eu in &output.entity_updates {
let id = self.knowledge.add_entity(&eu.name, &eu.entity_type, -1);
for a in &eu.aliases {
self.knowledge.add_alias(a, id as i64);
}
}
if !output.entries.is_empty() || !output.entity_updates.is_empty() {
self.flush()?;
}
Ok(output)
}
}
impl HDF5Memory {
pub fn tick_session(&mut self) -> Result<()> {
let d = self.config.decay_factor;
for w in self.cache.activation_weights.iter_mut() {
*w *= d;
}
self.flush()?;
if let Some(ref mut w) = self.wal {
w.truncate()?;
}
Ok(())
}
/// Number of pending WAL entries (0 if WAL disabled).
pub fn wal_pending_count(&self) -> usize {
self.wal.as_ref().map_or(0, |w| w.pending_count() as usize)
}
/// Explicit WAL merge: flush .h5, truncate WAL.
pub fn flush_wal(&mut self) -> Result<()> {
self.flush()?;
if let Some(ref mut w) = self.wal {
w.truncate()?;
}
Ok(())
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Ephemeral tier integration
// ─────────────────────────────────────────────────────────────────────────────
impl HDF5Memory {
/// Enable the ephemeral working memory tier with the given configuration.
pub fn enable_ephemeral(&mut self, config: EphemeralConfig) {
self.ephemeral = Some(EphemeralStore::new(config));
}
/// Return a shared reference to the ephemeral store, if enabled.
pub fn ephemeral(&self) -> Option<&EphemeralStore> {
self.ephemeral.as_ref()
}
/// Return a mutable reference to the ephemeral store, if enabled.
pub fn ephemeral_mut(&mut self) -> Option<&mut EphemeralStore> {
self.ephemeral.as_mut()
}
/// Promote frequently-accessed ephemeral entries into the persistent cache.
///
/// Every entry whose `access_count >= min_access_count` is removed from the
/// ephemeral store and written to the HDF5 cache, then the file is flushed.
/// Returns the number of entries promoted.
pub fn promote_ephemeral(&mut self, min_access_count: u32) -> Result<usize> {
let candidates = match &self.ephemeral {
None => return Ok(0),
Some(s) => s.promotion_candidates(min_access_count),
};
if candidates.is_empty() {
return Ok(0);
}
let dim = self.config.embedding_dim;
let mut promoted = 0;
for key in candidates {
let entry = match self
.ephemeral
.as_mut()
.and_then(|s| s.take_for_promotion(&key))
{
Some(e) => e,
None => continue,
};
let chunk = entry
.text
.clone()
.unwrap_or_else(|| String::from_utf8_lossy(&entry.value).into_owned());
let embedding = entry.embedding.clone().unwrap_or_else(|| vec![0.0f32; dim]);
self.cache.push(
chunk,
embedding,
format!("ephemeral::{key}"),
entry.created_at,
String::new(),
entry.tags.join(","),
);
promoted += 1;
}
if promoted > 0 {
self.flush()?;
}
Ok(promoted)
}
/// Search both the persistent HDF5 tier and the ephemeral tier, returning
/// the top `k` results sorted by score descending.
///
/// Ephemeral results are boosted by a factor of 1.2 to surface recent
/// in-context information above older persisted data.
pub fn unified_search(
&mut self,
query_embedding: &[f32],
query_text: &str,
k: usize,
) -> Vec<SearchResult> {
// Persistent tier.
let persistent = self.hybrid_search(query_embedding, query_text, 0.7, 0.3, k);
const EPHEMERAL_BOOST: f32 = 1.2;
let mut results = persistent;
if self.ephemeral.is_none() {
return results;
}
let eph = self.ephemeral.as_mut().unwrap();
// Collect (key, score) pairs from ephemeral — borrow ends before we
// access entries again below.
let eph_hits: Vec<(String, f32)> = if !query_embedding.is_empty() {
eph.search_embedding(query_embedding, k)
} else if !query_text.is_empty() {
eph.search_text(query_text, k)
} else {
Vec::new()
};
for (key, score) in &eph_hits {
if let Some(entry) = eph.get_entry(key) {
let chunk = entry
.text
.clone()
.unwrap_or_else(|| String::from_utf8_lossy(&entry.value).into_owned());
results.push(SearchResult {
score: score * EPHEMERAL_BOOST,
chunk,
index: usize::MAX,
timestamp: entry.created_at,
source_channel: format!("ephemeral::{key}"),
activation: 1.0,
});
}
}
results.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(k);
results
}
}
// --- Tests ---
#[cfg(test)]
@@ -1676,196 +1869,3 @@ mod tests {
assert!((mem.cache.tombstone_fraction() - 0.50).abs() < 0.01);
}
}
impl HDF5Memory {
pub fn set_strategy(&mut self, s: Box<dyn MemoryStrategy>) {
self.strategy = Some(s);
}
pub fn record(&mut self, exchange: Exchange) -> Result<StrategyOutput> {
let strat = self.strategy.as_ref().ok_or_else(|| {
MemoryError::Schema(
"strategy not initialized: call set_strategy() before record()".to_owned(),
)
})?;
let view = memory_strategy::CacheStoreView::new(&self.cache, &self.knowledge);
let output = strat.evaluate(&exchange, &view);
for e in &output.entries {
self.cache.push(
e.chunk.clone(),
e.embedding.clone(),
e.source_channel.clone(),
e.timestamp,
e.session_id.clone(),
e.tags.clone(),
);
}
for eu in &output.entity_updates {
let id = self.knowledge.add_entity(&eu.name, &eu.entity_type, -1);
for a in &eu.aliases {
self.knowledge.add_alias(a, id as i64);
}
}
if !output.entries.is_empty() || !output.entity_updates.is_empty() {
self.flush()?;
}
Ok(output)
}
}
impl HDF5Memory {
pub fn tick_session(&mut self) -> Result<()> {
let d = self.config.decay_factor;
for w in self.cache.activation_weights.iter_mut() {
*w *= d;
}
self.flush()?;
if let Some(ref mut w) = self.wal {
w.truncate()?;
}
Ok(())
}
/// Number of pending WAL entries (0 if WAL disabled).
pub fn wal_pending_count(&self) -> usize {
self.wal.as_ref().map_or(0, |w| w.pending_count() as usize)
}
/// Explicit WAL merge: flush .h5, truncate WAL.
pub fn flush_wal(&mut self) -> Result<()> {
self.flush()?;
if let Some(ref mut w) = self.wal {
w.truncate()?;
}
Ok(())
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Ephemeral tier integration
// ─────────────────────────────────────────────────────────────────────────────
impl HDF5Memory {
/// Enable the ephemeral working memory tier with the given configuration.
pub fn enable_ephemeral(&mut self, config: EphemeralConfig) {
self.ephemeral = Some(EphemeralStore::new(config));
}
/// Return a shared reference to the ephemeral store, if enabled.
pub fn ephemeral(&self) -> Option<&EphemeralStore> {
self.ephemeral.as_ref()
}
/// Return a mutable reference to the ephemeral store, if enabled.
pub fn ephemeral_mut(&mut self) -> Option<&mut EphemeralStore> {
self.ephemeral.as_mut()
}
/// Promote frequently-accessed ephemeral entries into the persistent cache.
///
/// Every entry whose `access_count >= min_access_count` is removed from the
/// ephemeral store and written to the HDF5 cache, then the file is flushed.
/// Returns the number of entries promoted.
pub fn promote_ephemeral(&mut self, min_access_count: u32) -> Result<usize> {
let candidates = match &self.ephemeral {
None => return Ok(0),
Some(s) => s.promotion_candidates(min_access_count),
};
if candidates.is_empty() {
return Ok(0);
}
let dim = self.config.embedding_dim;
let mut promoted = 0;
for key in candidates {
let entry = match self
.ephemeral
.as_mut()
.and_then(|s| s.take_for_promotion(&key))
{
Some(e) => e,
None => continue,
};
let chunk = entry
.text
.clone()
.unwrap_or_else(|| String::from_utf8_lossy(&entry.value).into_owned());
let embedding = entry.embedding.clone().unwrap_or_else(|| vec![0.0f32; dim]);
self.cache.push(
chunk,
embedding,
format!("ephemeral::{key}"),
entry.created_at,
String::new(),
entry.tags.join(","),
);
promoted += 1;
}
if promoted > 0 {
self.flush()?;
}
Ok(promoted)
}
/// Search both the persistent HDF5 tier and the ephemeral tier, returning
/// the top `k` results sorted by score descending.
///
/// Ephemeral results are boosted by a factor of 1.2 to surface recent
/// in-context information above older persisted data.
pub fn unified_search(
&mut self,
query_embedding: &[f32],
query_text: &str,
k: usize,
) -> Vec<SearchResult> {
// Persistent tier.
let persistent = self.hybrid_search(query_embedding, query_text, 0.7, 0.3, k);
const EPHEMERAL_BOOST: f32 = 1.2;
let mut results = persistent;
if self.ephemeral.is_none() {
return results;
}
let eph = self.ephemeral.as_mut().unwrap();
// Collect (key, score) pairs from ephemeral — borrow ends before we
// access entries again below.
let eph_hits: Vec<(String, f32)> = if !query_embedding.is_empty() {
eph.search_embedding(query_embedding, k)
} else if !query_text.is_empty() {
eph.search_text(query_text, k)
} else {
Vec::new()
};
for (key, score) in &eph_hits {
if let Some(entry) = eph.get_entry(key) {
let chunk = entry
.text
.clone()
.unwrap_or_else(|| String::from_utf8_lossy(&entry.value).into_owned());
results.push(SearchResult {
score: score * EPHEMERAL_BOOST,
chunk,
index: usize::MAX,
timestamp: entry.created_at,
source_channel: format!("ephemeral::{key}"),
activation: 1.0,
});
}
}
results.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(k);
results
}
}
+63 -63
View File
@@ -748,6 +748,69 @@ impl MemoryBackend for ClawhdfBackend {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Ephemeral tier methods on ClawhdfBackend
// ─────────────────────────────────────────────────────────────────────────────
impl ClawhdfBackend {
/// Enable the ephemeral (in-memory only) working memory tier.
pub fn enable_ephemeral(&mut self, config: crate::ephemeral::EphemeralConfig) {
self.memory.enable_ephemeral(config);
}
/// Store a text value in ephemeral memory.
///
/// Returns an error string if the ephemeral tier has not been enabled.
pub fn ephemeral_set(
&mut self,
key: &str,
value: &str,
ttl_secs: Option<f64>,
) -> Result<(), String> {
match self.memory.ephemeral_mut() {
Some(s) => {
s.set_text(key, value, ttl_secs);
Ok(())
}
None => Err("ephemeral tier not enabled".to_string()),
}
}
/// Retrieve a text value from ephemeral memory.
///
/// Returns `None` if the tier is disabled, the key is absent, or the
/// entry has expired.
pub fn ephemeral_get(&mut self, key: &str) -> Option<String> {
self.memory
.ephemeral_mut()?
.get_text(key)
.map(|s| s.to_string())
}
/// Delete a key from ephemeral memory.
///
/// Returns `true` if the key existed and was removed.
pub fn ephemeral_delete(&mut self, key: &str) -> bool {
self.memory.ephemeral_mut().is_some_and(|s| s.delete(key))
}
/// Return a snapshot of ephemeral tier statistics, or `None` if the tier
/// is not enabled.
pub fn ephemeral_stats(&self) -> Option<crate::ephemeral::EphemeralStats> {
self.memory.ephemeral().map(|s| s.stats())
}
/// Promote frequently-accessed ephemeral entries to persistent HDF5 storage.
///
/// Entries with `access_count >= min_access_count` are moved from the
/// ephemeral store into the persistent cache. Returns the count promoted.
pub fn promote_ephemeral(&mut self, min_access_count: u32) -> Result<usize, String> {
self.memory
.promote_ephemeral(min_access_count)
.map_err(|e| e.to_string())
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
@@ -1333,66 +1396,3 @@ mod tests {
assert!(out.starts_with("# Title"));
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Ephemeral tier methods on ClawhdfBackend
// ─────────────────────────────────────────────────────────────────────────────
impl ClawhdfBackend {
/// Enable the ephemeral (in-memory only) working memory tier.
pub fn enable_ephemeral(&mut self, config: crate::ephemeral::EphemeralConfig) {
self.memory.enable_ephemeral(config);
}
/// Store a text value in ephemeral memory.
///
/// Returns an error string if the ephemeral tier has not been enabled.
pub fn ephemeral_set(
&mut self,
key: &str,
value: &str,
ttl_secs: Option<f64>,
) -> Result<(), String> {
match self.memory.ephemeral_mut() {
Some(s) => {
s.set_text(key, value, ttl_secs);
Ok(())
}
None => Err("ephemeral tier not enabled".to_string()),
}
}
/// Retrieve a text value from ephemeral memory.
///
/// Returns `None` if the tier is disabled, the key is absent, or the
/// entry has expired.
pub fn ephemeral_get(&mut self, key: &str) -> Option<String> {
self.memory
.ephemeral_mut()?
.get_text(key)
.map(|s| s.to_string())
}
/// Delete a key from ephemeral memory.
///
/// Returns `true` if the key existed and was removed.
pub fn ephemeral_delete(&mut self, key: &str) -> bool {
self.memory.ephemeral_mut().is_some_and(|s| s.delete(key))
}
/// Return a snapshot of ephemeral tier statistics, or `None` if the tier
/// is not enabled.
pub fn ephemeral_stats(&self) -> Option<crate::ephemeral::EphemeralStats> {
self.memory.ephemeral().map(|s| s.stats())
}
/// Promote frequently-accessed ephemeral entries to persistent HDF5 storage.
///
/// Entries with `access_count >= min_access_count` are moved from the
/// ephemeral store into the persistent cache. Returns the count promoted.
pub fn promote_ephemeral(&mut self, min_access_count: u32) -> Result<usize, String> {
self.memory
.promote_ephemeral(min_access_count)
.map_err(|e| e.to_string())
}
}
+1 -1
View File
@@ -786,7 +786,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let wal_path = dir.path().join("test.h5.wal");
let unicode_chunk = "Hello 世界! 🌍 émojis & ünïcödé";
let embedding = vec![0.1, -0.2, 3.14159, f32::MAX, f32::MIN_POSITIVE];
let embedding = vec![0.1, -0.2, 3.4567, f32::MAX, f32::MIN_POSITIVE];
{
let mut wal = WalFile::open(&wal_path).unwrap();
let entry = WalEntry {
+2 -2
View File
@@ -1048,7 +1048,7 @@ fn test_gpu_l2_fallback_works() {
let tombstones = vec![0u8; 3];
let gpu = clawhdf5_agent::gpu_search::GpuSearchBackend::try_init(&vectors, &norms, 2, 1);
let results = gpu.search_l2(&vec![0.0, 0.0], &vectors, &tombstones, 3);
let results = gpu.search_l2(&[0.0, 0.0], &vectors, &tombstones, 3);
assert_eq!(results.len(), 3);
assert_eq!(results[0].0, 0);
@@ -1099,7 +1099,7 @@ fn test_mmap_reader_direct_access() {
// Open via MmapReader directly
let mmap = clawhdf5_io::MmapReader::open(&path).unwrap();
assert!(mmap.len() > 0);
assert!(!mmap.is_empty());
// Verify we can read bytes at specific offsets
let bytes = mmap.read_at(0, 8);
assert!(bytes.is_some());
@@ -137,10 +137,10 @@ fn bench_hit_at_1_1014_records() {
0.3,
1,
);
if let Some((top_idx, _)) = results.first() {
if *top_idx == target_indices[qi] {
hits += 1;
}
if let Some((top_idx, _)) = results.first()
&& *top_idx == target_indices[qi]
{
hits += 1;
}
}