Implement INT-01, INT-16, INT-17, INT-18: hybrid weight fix, BM25 cache, SA guard, deny.toml
INT-01: Change hybrid search default weights from 0.7/0.3 to 0.4/0.6 (vector/keyword) in openclaw.rs and lib.rs call sites, and update the async_memory.rs doc comment. LongMemEval benchmarks show 0.4/0.6 strictly dominates 0.7/0.3 on Hit@1, Hit@5, Hit@10, and MRR at both turn and session granularity. INT-16: Cache BM25 index in HDF5Memory to avoid O(N×terms) rebuild on every hybrid_search call. Index is lazily built on first search and invalidated (set to None) by every write path: save(), save_or_update(), save_batch(), delete(), compact(). Uses take()/put-back to avoid borrow conflicts with &mut self in vector_keyword_search. INT-17: Clamp decay_factor to [0.0, 1.0) in spreading_activation. A caller passing decay_factor >= 1.0 would cause activation to accumulate unboundedly through cycles for the full max_steps duration. Clamping guarantees convergence. INT-18: Add deny.toml at workspace root for cargo-deny. Enforces MIT-compatible licenses, warns on duplicate semver-major versions, and flags unmaintained crates. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4b17bf9101
commit
5ca0b8092e
@@ -37,7 +37,7 @@
|
|||||||
//! let mem = AsyncHDF5Memory::open_with(path, config).await?;
|
//! let mem = AsyncHDF5Memory::open_with(path, config).await?;
|
||||||
//! mem.save(entry).await?; // buffered → background writer
|
//! mem.save(entry).await?; // buffered → background writer
|
||||||
//! mem.save_batch(entries).await?; // also buffered
|
//! mem.save_batch(entries).await?; // also buffered
|
||||||
//! let results = mem.hybrid_search(emb, "query".into(), 0.7, 0.3, 5).await;
|
//! let results = mem.hybrid_search(emb, "query".into(), 0.4, 0.6, 5).await;
|
||||||
//! mem.shutdown().await?; // final flush + stop
|
//! mem.shutdown().await?; // final flush + stop
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
|||||||
@@ -439,6 +439,11 @@ impl KnowledgeCache {
|
|||||||
min_activation: f32,
|
min_activation: f32,
|
||||||
max_steps: usize,
|
max_steps: usize,
|
||||||
) -> Vec<(u64, f32)> {
|
) -> Vec<(u64, f32)> {
|
||||||
|
// decay_factor >= 1.0 means activation never diminishes, so propagation
|
||||||
|
// through cycles accumulates unboundedly for the full max_steps duration.
|
||||||
|
// Clamp to [0.0, 1.0) to guarantee convergence.
|
||||||
|
let decay_factor = decay_factor.clamp(0.0, 1.0 - f32::EPSILON);
|
||||||
|
|
||||||
let mut activation: HashMap<u64, f32> = HashMap::new();
|
let mut activation: HashMap<u64, f32> = HashMap::new();
|
||||||
|
|
||||||
// Initialise seeds with activation 1.0.
|
// Initialise seeds with activation 1.0.
|
||||||
|
|||||||
@@ -227,6 +227,10 @@ pub struct HDF5Memory {
|
|||||||
/// search.
|
/// search.
|
||||||
#[cfg(feature = "hnsw")]
|
#[cfg(feature = "hnsw")]
|
||||||
hnsw_synced_len: usize,
|
hnsw_synced_len: usize,
|
||||||
|
/// Cached BM25 index. Rebuilt lazily on the first `hybrid_search` call
|
||||||
|
/// after any write; set to `None` on every save / delete / compact to
|
||||||
|
/// ensure it is never stale.
|
||||||
|
bm25_cache: Option<bm25::BM25Index>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for HDF5Memory {
|
impl std::fmt::Debug for HDF5Memory {
|
||||||
@@ -266,6 +270,7 @@ impl HDF5Memory {
|
|||||||
hnsw_dirty: false,
|
hnsw_dirty: false,
|
||||||
#[cfg(feature = "hnsw")]
|
#[cfg(feature = "hnsw")]
|
||||||
hnsw_synced_len: 0,
|
hnsw_synced_len: 0,
|
||||||
|
bm25_cache: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +306,7 @@ impl HDF5Memory {
|
|||||||
hnsw_dirty: true,
|
hnsw_dirty: true,
|
||||||
#[cfg(feature = "hnsw")]
|
#[cfg(feature = "hnsw")]
|
||||||
hnsw_synced_len: 0,
|
hnsw_synced_len: 0,
|
||||||
|
bm25_cache: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +523,7 @@ impl HDF5Memory {
|
|||||||
);
|
);
|
||||||
// In-place embedding change: the index node is stale, force rebuild.
|
// In-place embedding change: the index node is stale, force rebuild.
|
||||||
self.hnsw_mark_dirty();
|
self.hnsw_mark_dirty();
|
||||||
|
self.bm25_cache = None;
|
||||||
let needs_flush = self
|
let needs_flush = self
|
||||||
.wal
|
.wal
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -558,6 +565,7 @@ impl AgentMemory for HDF5Memory {
|
|||||||
entry.tags,
|
entry.tags,
|
||||||
);
|
);
|
||||||
self.hnsw_on_insert(idx);
|
self.hnsw_on_insert(idx);
|
||||||
|
self.bm25_cache = None;
|
||||||
let needs_flush = self
|
let needs_flush = self
|
||||||
.wal
|
.wal
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -586,6 +594,7 @@ impl AgentMemory for HDF5Memory {
|
|||||||
}
|
}
|
||||||
// Batch inserts rebuild the index once rather than node-by-node.
|
// Batch inserts rebuild the index once rather than node-by-node.
|
||||||
self.hnsw_mark_dirty();
|
self.hnsw_mark_dirty();
|
||||||
|
self.bm25_cache = None;
|
||||||
self.flush()?;
|
self.flush()?;
|
||||||
Ok(indices)
|
Ok(indices)
|
||||||
}
|
}
|
||||||
@@ -597,6 +606,7 @@ impl AgentMemory for HDF5Memory {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
self.hnsw_on_delete(id);
|
self.hnsw_on_delete(id);
|
||||||
|
self.bm25_cache = None;
|
||||||
self.flush()?;
|
self.flush()?;
|
||||||
|
|
||||||
// Auto-compact if threshold exceeded
|
// Auto-compact if threshold exceeded
|
||||||
@@ -614,6 +624,7 @@ impl AgentMemory for HDF5Memory {
|
|||||||
if removed > 0 {
|
if removed > 0 {
|
||||||
// Compaction renumbers cache indices; rebuild the index to match.
|
// Compaction renumbers cache indices; rebuild the index to match.
|
||||||
self.hnsw_mark_dirty();
|
self.hnsw_mark_dirty();
|
||||||
|
self.bm25_cache = None;
|
||||||
self.flush()?;
|
self.flush()?;
|
||||||
}
|
}
|
||||||
Ok(removed)
|
Ok(removed)
|
||||||
@@ -1586,7 +1597,7 @@ impl HDF5Memory {
|
|||||||
k: usize,
|
k: usize,
|
||||||
) -> Vec<SearchResult> {
|
) -> Vec<SearchResult> {
|
||||||
// Persistent tier.
|
// Persistent tier.
|
||||||
let persistent = self.hybrid_search(query_embedding, query_text, 0.7, 0.3, k);
|
let persistent = self.hybrid_search(query_embedding, query_text, 0.4, 0.6, k);
|
||||||
const EPHEMERAL_BOOST: f32 = 1.2;
|
const EPHEMERAL_BOOST: f32 = 1.2;
|
||||||
let mut results = persistent;
|
let mut results = persistent;
|
||||||
|
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ impl MemoryBackend for ClawhdfBackend {
|
|||||||
let candidates = k.saturating_mul(3).max(10);
|
let candidates = k.saturating_mul(3).max(10);
|
||||||
let raw = self
|
let raw = self
|
||||||
.memory
|
.memory
|
||||||
.hybrid_search(query_embedding, query_text, 0.7, 0.3, candidates);
|
.hybrid_search(query_embedding, query_text, 0.4, 0.6, candidates);
|
||||||
|
|
||||||
if raw.is_empty() {
|
if raw.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|||||||
@@ -90,7 +90,16 @@ impl HDF5Memory {
|
|||||||
keyword_weight: f32,
|
keyword_weight: f32,
|
||||||
k: usize,
|
k: usize,
|
||||||
) -> Vec<SearchResult> {
|
) -> Vec<SearchResult> {
|
||||||
let bm25 = bm25::BM25Index::build(&self.cache.chunks, &self.cache.tombstones);
|
// Lazily build the BM25 index once and reuse across searches. The
|
||||||
|
// cache is invalidated (set to None) by every save / delete / compact
|
||||||
|
// call so it is never stale. We take() the index out of the Option
|
||||||
|
// so that we can pass &bm25 while also holding &mut self for the
|
||||||
|
// vector search path; it is put back immediately after.
|
||||||
|
if self.bm25_cache.is_none() {
|
||||||
|
self.bm25_cache =
|
||||||
|
Some(bm25::BM25Index::build(&self.cache.chunks, &self.cache.tombstones));
|
||||||
|
}
|
||||||
|
let bm25 = self.bm25_cache.take().expect("just built");
|
||||||
let scored = self.vector_keyword_search(
|
let scored = self.vector_keyword_search(
|
||||||
query_embedding,
|
query_embedding,
|
||||||
query_text,
|
query_text,
|
||||||
@@ -121,6 +130,9 @@ impl HDF5Memory {
|
|||||||
|
|
||||||
let hit_indices: Vec<usize> = results.iter().map(|r| r.index).collect();
|
let hit_indices: Vec<usize> = results.iter().map(|r| r.index).collect();
|
||||||
self.apply_hebbian_boost(&hit_indices);
|
self.apply_hebbian_boost(&hit_indices);
|
||||||
|
// Restore the BM25 index before flush so it survives the write.
|
||||||
|
// flush() does not invalidate bm25_cache; only mutating writes do.
|
||||||
|
self.bm25_cache = Some(bm25);
|
||||||
self.flush().ok();
|
self.flush().ok();
|
||||||
|
|
||||||
results
|
results
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# cargo-deny configuration for the clawhdf5 workspace.
|
||||||
|
# Run: cargo deny check
|
||||||
|
|
||||||
|
[graph]
|
||||||
|
targets = []
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
# Deny all crates with known security vulnerabilities.
|
||||||
|
version = 2
|
||||||
|
ignore = []
|
||||||
|
|
||||||
|
[licenses]
|
||||||
|
version = 2
|
||||||
|
# Allow MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib — all
|
||||||
|
# compatible with ClawHDF5's MIT license.
|
||||||
|
allow = [
|
||||||
|
"MIT",
|
||||||
|
"Apache-2.0",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"ISC",
|
||||||
|
"Zlib",
|
||||||
|
"Unicode-3.0",
|
||||||
|
"Unicode-DFS-2016",
|
||||||
|
"CC0-1.0",
|
||||||
|
]
|
||||||
|
# Emit a warning (not an error) for licenses that need manual review.
|
||||||
|
exceptions = []
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
# Warn on multiple versions of the same crate; error only on exact duplicates
|
||||||
|
# at the same semver major to avoid false positives during dep graph churn.
|
||||||
|
multiple-versions = "warn"
|
||||||
|
wildcards = "allow"
|
||||||
|
highlight = "all"
|
||||||
|
|
||||||
|
# Deny known-unmaintained crates.
|
||||||
|
deny = []
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
unknown-registry = "warn"
|
||||||
|
unknown-git = "warn"
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
|
allow-git = []
|
||||||
Reference in New Issue
Block a user