Every Python interop suite had stopped running on this machine: the h5py writer round-trips, the facade suite, netCDF4 and the reference files. `python3` is 3.14, nothing on the box has h5py, and PEP 668 refuses to install it into a system interpreter at all — so the availability probes all returned false and each suite skipped without failing. A silent skip here is exactly how the v5 compound-datatype bug reached a release, so the probes now read `CLAWHDF5_PYTHON` and `ci-test.sh` picks up `.venv/bin/python` on its own. The detection sits at the top of the script rather than beside the interop step, because the non-ignored suites run in the earlier `cargo test` step and would otherwise still miss it. `CLAWHDF5_REQUIRE_INTEROP=1` continues to turn a skip into a failure. Verified against a venv with h5py 3.16 / HDF5 2.0.0: 94 interop tests across the four suites, all passing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
46 KiB
46 KiB
Changelog
Unreleased
Testing
- The Python interop suites honour
CLAWHDF5_PYTHON, andci-test.shpicks up a.venv/bin/pythonautomatically. On a PEP 668 "externally managed" system h5py cannot be installed into the system interpreter at all, so every interop suite — the h5py writer round-trips, the facade, netCDF4 and the reference files — was skipping silently. A silent skip here is exactly how the v5 compound-datatype bug reached a release.CLAWHDF5_REQUIRE_INTEROP=1still turns a skip into a failure.
Memory
clawhdf5-agent:MemoryConfig::quantized_indexstores the vector index's own copy of the embeddings asi8rather thanf32, which at 100k 384-dim entries takes the index from 266 to 123 MiB and the whole reopened store from 399 to 256 MiB (2.72x -> 1.74x the raw vectors). Quantised distances are approximate andefcannot compensate — recall@10 tops out at 0.967 against f32's 0.9995 — so the query path re-scores the candidate pool against the exact embeddings the store already holds, which restores recall (0.9940 vs 0.9945 at ef=64) for about 13% of QPS. Off by default: it trades query speed for memory, and which side is worth more depends on the deployment. The setting is persisted, so a reopened store does not silently revert to four times the index memory.clawhdf5-ann:Storage::Int8and thebuild_with/new_with/from_graph_bytes_withconstructors that select it. The scale is per row, not global — a fixed[-1, 1]scale spends fewer than 12 of the 255 levels on a unit-length 128-dim vector and is unusable (0.35 top-10 overlap against an exact ranking, versus 0.99 per row).compact()keeps the storage it was given; serialized indexes still carry f32 vectors, so a quantised index is rebuilt rather than loaded.
Memory
clawhdf5-agent: a loaded store holds ~30% less memory (100k 384-dim entries: 505 -> 357 MiB, 3.44x -> 2.43x the raw vectors). The cache kept every embedding twice — aVec<Vec<f32>>and a flattened copy for the batched kernels, maintained in lock-step — so it now stores only the flat buffer and indexes into it. Recall and query latency are unchanged. Breaking:MemoryCache::embeddingsis acache::Embeddingsrather than aVec<Vec<f32>>(indexing still yields a&[f32]row);embeddings_flatis gone, replaced byflat_embeddings();rebuild_flat()is a deprecated no-op. Rows are now always exactlydimlong — shorter ones are zero-padded — which makes the ragged-row case that used to silently misalign the flattened copy unrepresentable.clawhdf5-bench:search_harness --footprintreports live heap use per stage, measured with a counting allocator (RSS cannot see a structure freed into the allocator's own pool).
Unreleased
Retrieval quality
clawhdf5-agent: re-ranking discarded the retrieval score.reranker::rerankbuilt its combined score from temporal decay, source authority and Hebbian activation only —RerankInputhad no relevance field — so re-ranking a candidate pool reordered it by age and threw the retriever's ordering away. The OpenClaw backend re-ranked every search, so this was its shipping behaviour: measured over the full LongMemEval haystack it cost 40.6pp of Hit@1 (11.0% vs 51.6%) and two thirds of MRR (0.183 vs 0.643).RerankInput::relevanceandReRankConfig::relevance_weight(1.0 by default) fix it: relevance leads and the metadata signals break near-ties, which restores retrieval (Hit@1 +0.4pp vs no re-ranking) and improves recency discrimination by 6–7pp. Breaking:RerankInputandReRankConfiggained fields, so literal constructions need updating;..Default::default()does not.clawhdf5-bench: the LongMemEval harness feeds the dataset's real session dates to the store instead of a synthetic counter (decay needs true intervals, not just the right order), and reportsnewest_gold_first— on aknowledge-updatequestion, did the newest gold session outrank the stale one it supersedes? Plain recall cannot see this, because both are labelled gold. New--rerank-sweep.
v2.5.0 (2026-09-19)
Upgrade Notes
- Retrieval rankings change, for the better. The default fusion weights
move from
0.7/0.3to0.4/0.6(hybrid::DEFAULT_FUSION), measured over the full LongMemEval haystack: turn-level Hit@1 51.6% vs 44.2%, MRR 0.643 vs 0.586.unified_searchand the OpenClaw backend pick this up automatically; callers passing weights tohybrid_searchexplicitly are unaffected. - Out-of-range selections are now errors.
read_*_selectionused to return data for a selection that ran past a dataset edge — a hyperslab came back zero-padded, and a point with an out-of-range coordinate wrapped into the next row. Both are nowFormatError::SelectionOutOfBounds. Code relying on the old (wrong) values will start seeing errors. - Large compressed datasets written without explicit chunk dimensions get a
different layout. They used to be stored as one chunk; they are now split
to ~1 MiB chunks. The files stay standard and h5py-readable, and explicit
with_chunksis unaffected. rayonis now a default dependency ofclawhdf5-agent(the parallel index build). Opt out with--no-default-features --features float16,hnsw.clawhdf5-annsearch results no longer shrink when records near the query have been deleted, so a search that previously returned fewer thankresults now returnsk.
Retrieval quality
clawhdf5-agent: optional keyword stemming —bm25::TokenFilter::StemmedandHDF5Memory::set_token_filter, so "training" and "trains" match. Off by default, on measurement rather than principle: over the full LongMemEval haystack it buys depth and costs the top rank (BM25 alone: Hit@5 +2.8pp, Hit@10 +2.4pp, Hit@1 −1.8pp, MRR unchanged), and on the shipping hybrid configuration the trade is narrower still. SeeBENCHMARKS.md.clawhdf5-agent:QueryExpander::expandpanicked on ordinary non-ASCII input —"İ AI"was enough. It searched a lowercased copy of the query and then sliced the original with those offsets, which only works while lowercasing preserves byte length (Turkishİis 2 bytes and lowercases to 3). Depending on where the offsets drifted it either corrupted the output ("İstanbul AI trip" lost a character) or panicked. Matching now walks the original string.clawhdf5-agent: query expansion no longer rewrites text inside words.replace_word_case_insensitivedid a plain substring replace despite its name, so "training" became "trArtificial Intelligencening" and "programming" became "Pull Requestogramming" — every acronym expansion of ordinary prose was corrupt. Matches now require word boundaries; genuine acronyms (API,database) still expand.clawhdf5-agent: the default fusion weights are now the measured ones. A sweep of every 0.1 step over the full LongMemEval haystack (500 questions, real MiniLM embeddings) shows the long-standing0.7/0.3default is strictly dominated by0.4/0.6— turn-level Hit@1 51.6% vs 44.2%, Hit@5 81.4% vs 79.2%, Hit@10 87.8% vs 85.8%, MRR 0.643 vs 0.586, and better at session level too. The finding was recorded inBENCHMARKS.mdbut had never been applied:unified_searchand the OpenClaw backend both hardcoded0.7/0.3. They now usehybrid::DEFAULT_FUSION. Callers passing weights tohybrid_searchexplicitly are unaffected — pass0.4/0.6(or usehybrid_search_with) to get the tuned behaviour.clawhdf5-agent: fusion is now selectable. Newhybrid::Fusion(Weighted { vector, keyword }orRrf { k }),hybrid::fuse,hybrid::hybrid_search_fusedandHDF5Memory::hybrid_search_with. Reciprocal rank fusion existed but was unreachable from the store, so it had never been measured against the weighted sum; the LongMemEval bench now has anRRFmode.
HDF5 Read Path
- Selection reads cost what the selection costs.
read_*_selectiondecoded the entire dataset and then picked elements out, so a 64 x 64 window of a 64 MB compressed dataset took 105 ms - about as long as reading all of it. Now only the rows (contiguous) or chunks that overlap the selection's bounding box are read and decompressed: that window takes 0.39 ms, one row 2.7 ms, one column 5.2 ms. Results are identical to the full-read path (equivalence-tested over random hyperslabs and point lists, ranks 1-3, contiguous / chunked / deflate). Newread_harnessbench binary. - Faster full reads (same-moment A/B, 64 MB
f64): chunked + deflate 110 -> 69 ms, chunked 72 -> 60 ms, contiguous 56 -> 30 ms. The facade's cached read path now decompresses cache misses in parallel batches (it was sequential; only the uncached reader was parallel) and caches only datasets that fit the chunk cache; unfiltered chunks are copied straight from the file bytes; a contiguous dataset is converted straight from the file bytes; and the native-endian conversions no longer zero a buffer before overwriting it. - Datasets indexed by a version-2 B-tree now read (layout v4, chunk index
type 5 — what
libver='latest'uses for two or more unlimited dimensions; previously "unsupported chunked layout"). The four copies of the chunk-index dispatch are now one shared function, so every read path gets it. H5T_STD_REFreferences (HDF5 1.12+, datatype message version 4) parse:ReferenceTypegainsObject2,DatasetRegion2andAttribute, andread_object_referencesdecodes the new object references. Previously any dataset of this type failed withInvalidReferenceType(2). Tested against a file written by HDF5 2.0 itself (fixture + generator script committed).- Automatic chunk sizes. Asking for compression (or any filter) without
with_chunksused to store the whole dataset as one chunk, so any read had to decompress everything and nothing could be decoded in parallel. Datasets up to 1 MiB stay a single chunk, as before; larger ones are split by halving the dimensions in turn until a chunk is at most 1 MiB (the approach h5py takes). Behaviour change: large compressed datasets written without explicit chunk dimensions get a different (standard, h5py-readable) layout. Explicitwith_chunksis unaffected. - Out-of-range selections are errors. They used to return data: a hyperslab
past an edge came back padded with zeros, and a point whose column was out of
range wrapped into the next row and returned that element. Now
FormatError::SelectionOutOfBounds(also for a rank mismatch or overlapping blocks).
Search
clawhdf5-ann: faster index builds. Back-link pruning is 90% of a build's distance evaluations; the bulk build now inserts in batches and prunes each overflowing neighbour list once per batch (10K: 1676 -> 1074 ms). With theparallelfeature, planning and pruning run on a thread pool (10K: 388 ms, 100K: ~21 s -> 5.9 s on 16 cores). The graph is deterministic and identical with or without the feature.clawhdf5-agent'sparallelfeature enables it for the agent's index and is now on by default (addsrayonto the default dependency set; build with--no-default-features --features float16,hnswto opt out).clawhdf5-ann:HnswIndex::searchreturned fewer thankresults — often none — when the records nearest the query had been deleted: it collectedefcandidates, then dropped the deleted ones, then tookk. Deleted nodes are now traversed as waypoints but never occupy a result slot, so a search returns theknearest live records. Matters for any store that deletes or supersedes memories without compacting straight away.
v2.4.0 (2026-09-19)
Upgrade Notes
- Search results improve on upgrade. The HNSW index now reaches true
neighbours it previously could not (recall@10 0.31 -> 0.98 at 100K records on
clustered data), so
hybrid_searchrankings change for the better. The agent rebuilds its index from the store automatically; a standaloneHnswIndexpersisted withto_hdf5_byteskeeps its old graph until rebuilt. hybrid_searchno longer writes the store. Hebbian activation boosts are persisted by the next checkpoint (any flushing write,flush_wal, or when theHDF5Memoryis dropped) instead of inside every query; a crash before then forgets only the boosts since the last checkpoint. Activation weights are now capped at 16.- A new sidecar file,
<store>.h5.ann, holds the vector index graph. It is derived data: safe to delete (the index is rebuilt), copied bysnapshot(), and worth including when copying a store by hand to avoid a rebuild. BM25Indexno longer caches IDF and gainedadd_document,remove_document,pad_to,scores,lenandis_empty; results are now deterministic (ties break by record id).
Search
clawhdf5-ann: HNSW recall fix. Neighbours were chosen as the plain closest-M, which on clustered data (what embeddings look like) turns each cluster into an island: recall@10 was 0.87 / 0.67 / 0.31 at 1K / 10K / 100K vectors and did not improve withef. The index now uses the HNSW paper's diversity heuristic (Algorithm 4 with kept pruned connections) when linking a new node and when pruning back-links: recall@10 atef = 64is 1.00 / 1.00 / 0.98 and responds toef. Builds are slower (~3.5x at 10K). Existing persisted indexes keep their old graph until rebuilt; the agent rebuilds its index from the cache, so stores pick this up automatically.clawhdf5-agent:hybrid_searchis 23-39x faster in steady state (p50 5.5 -> 0.24 ms at 1K records, 49 -> 2.1 ms at 10K, 884 -> 23 ms at 100K). Every query used to rebuild the BM25 index from scratch and rewrite the whole.h5file. The keyword index now lives for the life of the store and is updated incrementally (add / remove / in-place update, exactly equivalent to a fresh build - property-tested), and a query no longer writes the store. Behaviour change: Hebbian activation boosts are persisted by the next checkpoint (any flushing write,flush_wal, or drop) rather than immediately; a crash in between forgets only the boosts since the last checkpoint. Activation weights are now capped (16.0) - they grew without bound.clawhdf5-agent: the vector index is persisted, soopen()no longer rebuilds it on the first search (first query after open: 2627 -> 15 ms at 10K records, 36 s -> 159 ms at 100K). The HNSW graph — not the vectors, which the store already holds — is written to<store>.h5.annat each checkpoint and tied to it by a generation id in/meta; a missing, stale, damaged or structurally invalid sidecar is ignored and the index rebuilt. Records replayed from the WAL join the loaded index incrementally; a replayed update or delete invalidates it.snapshot()copies it. Batch saves no longer force a full index rebuild.clawhdf5-ann: faster HNSW build and search with identical recall. The cosine metric stores unit vectors and compares them with a plain dot product (it re-derived both norms on every distance evaluation), and the per-callHashSetof visited nodes is a reusable epoch-stamped array. Build 2.75 -> 1.89 s at 10K and ~38 -> 21 s at 100K; QPS atef = 6422.7K -> 39K at 10K. Distances returned bysearchare unchanged (1 - cosine). Indexes loaded from older HDF5 files are normalised on load.clawhdf5-accel: the SIMD backend is detected once per process instead of on every kernel call.clawhdf5-ann:HnswIndex::graph_to_bytes/from_graph_bytes— graph-only serialization (checksummed, every neighbour id and level validated on load).clawhdf5-agent: a further 4-5x onhybrid_searchwith identical rankings (p50 now 0.07 / 0.49 / 4.65 ms at 1K / 10K / 100K — 79x / 100x / 190x faster than v2.3.0). Fusion needs every keyword score but not their ranking: newBM25Index::scoresreturns them unsorted from a dense accumulator (it hashed every posting, then sorted every match), andmerge_vector_keywordselects its top k instead of sorting every candidate. Capping the keyword candidate pool was measured and rejected: it changes the top-10 for most queries (search_harness --fusion-study).clawhdf5-agent: BM25 results are deterministic (ties break by record id), top-k uses a bounded heap, and the "WAND early termination" that computed a bound and then ignored it is gone. IDF is computed per query.clawhdf5-bench: newsearch_harnessbinary — HNSW recall@10 / QPS / latency perefagainst an exact scan, and end-to-endhybrid_searchtimings, on deterministic clustered (or--uniform) data. Baseline inBENCHMARKS.md.
v2.3.0 (2026-09-19)
Upgrade Notes
- A memory store now has a single writer.
HDF5Memory::create/opentake an exclusive lock (<store>.h5.lock); a second open of the same store — in the same or another process — returnsMemoryError::Locked. Code that opened a second handle just to read should useHDF5Memory::open_read_only. - Unsigned array attributes arrive as
AttrValue::U64Array, notI64Array, andattrs()may now returnAttrValue::Raw. Exhaustive matches onAttrValueneed the two new arms. - WAL header version 3 → 4. v3 files are read and upgraded in place, but a
store written by 2.3.0 with a pending WAL cannot be opened by 2.2.0 or
earlier (it is refused, not corrupted). Checkpoint first
(
flush_wal) if you need to downgrade. MemoryConfig::compressionnow uses deflate unless the agent's newzstdfeature is enabled; it previously failed outright in a default build.MemoryErrorgainedLocked;FormatErrorgainedUnresolvedSharedMessage,ExternalDataFilesUnsupportedandExternalLinkUnsupported;MessageTypegainedExternalDataFiles.
Bug Fixes
clawhdf5-format: compound datatypes written with default libver bounds (datatype message version 1 — what plainh5py.File(path, 'w')produces) were mis-parsed. The v1 member layout carries 28 bytes of legacy array fields after the byte offset (the parser skipped 24), and v2 pads member names to 8 bytes and has no array fields at all (the parser did neither), so every member after the first byte offset was read from the wrong position — typically surfacing asOverflow("compound member ...")on read. Found by adding a default-libver axis to the h5py interop tests; byte-level regression tests for v1 and v2 added.clawhdf5-gpu:gpu_testscould hang forever under the default parallel test runner — every test created its own wgpu instance and device at once. Tests now serialise GPU access, and GPU→CPU readback waits are bounded (30 s) so a wedged driver returnsGpuError::BufferMapinstead of blocking.clawhdf5-agent:benches/bench.rsandbenches/memory_bench.rsno longer compiled against the currentstrategy/consolidationAPIs.
HDF5 Compatibility
clawhdf5-format/clawhdf5: datasets and attributes that use a committed (named) datatype now read correctly. They store a shared-message reference; the facade parsed the reference bytes as the datatype (Time { size: 0 }, unreadable data) and silently dropped such attributes. The shared-reference parser itself was wrong for real files: version 2 has no reserved bytes, and the version 3 types were inverted (1 = SOHM heap, 2 = committed).- Fill values are applied on read. There was no Fill Value message parser:
the holes of a sparse chunked dataset read as zeros even when the fill value
was not zero (silently wrong data), and a dataset that was created but never
written failed with
NoDataAllocatedwhere h5py returns a filled array. Messages v1–v3 and the old 0x0004 form are parsed; the fill value is written into exactly the chunk-grid cells missing from the chunk index. - Soft links are followed during path resolution, in old- and new-style groups (absolute/relative targets, links to groups, links through links), with a depth limit so a link cycle is an error rather than a hang. A dangling link reports the target it could not find.
- Things the reader does not follow are now explicit errors instead of wrong
answers: an external link is
ExternalLinkUnsupported { filename, object_path }(wasPathNotFound), and a dataset whose raw data lives in external files (message 0x0007, now a knownMessageType) isExternalDataFilesUnsupported(it would otherwise read as fill values). attrs()no longer drops attributes. Any attribute whose datatype had noAttrValuevariant was omitted with no error — including every Pythonbool(h5py storesattrs["flag"] = Trueas an enum), complex numbers, compound values and object references. Now:- numpy/h5py-style booleans (an enum of exactly
FALSE=0 /TRUE=1) decode asI64/I64Arrayof 0/1; - new
AttrValue::U64Arraykeeps unsigned arrays unsigned (they were cast toI64Array, so values abovei64::MAXcame back negative). Behaviour change: code matchingI64Arrayfor an unsigned attribute must also matchU64Array(the netCDF-4 CF helpers and Python bindings do); - new
AttrValue::Raw { datatype, shape, data }carries everything else verbatim, decodable withclawhdf5_format::data_readagainstdatatype. Both new variants are writable, so an attribute can be copied between files unchanged. Python receivesRawas{"dtype", "shape", "data"}.
- numpy/h5py-style booleans (an enum of exactly
- All of the above are covered by h5py interop tests under both default and
libver='latest'bounds, compared against h5py's own readback.
Security
clawhdf5: virtual-dataset source file names are untrusted input but were joined straight onto the opened file's directory, so a crafted file could make the reader open any path the process can reach (absolute path, or..components). Only plain relative paths inside that directory are accepted.
Durability & Integrity
clawhdf5-agent: a crash between writing a checkpoint and truncating the WAL no longer duplicates every pending entry on the next open. Each checkpoint records aWalMark(byte length + chained CRC of the WAL prefix it folded in) in/meta;open()skips exactly that prefix when it is still present. No WAL format change for this; older files behave as before.clawhdf5-agent: checkpoints and snapshots are durable as a unit — the temp file is synced before the rename and the directory after it. Individual WAL appends remain unsynced by design (documented inCLAUDE.md).clawhdf5-agent:save_or_updatehits are logged as a newUpdateWAL record, so replay updates in place instead of appending a duplicate. WAL header version 3 → 4 (so older builds refuse the file rather than truncating a record they can't parse); v3 files are read and upgraded in place.clawhdf5-agent: loading validates every per-record dataset length (a truncated store is nowMemoryError::Schema, not a later panic), fixes then.len() == n.len()tautology that trusted a norms dataset of any length, and rejectsembedding_dim == 0with records present.clawhdf5-agent: eight behaviouralMemoryConfigfields are now persisted in/meta. Previously they reset to defaults on every open — a compressed store was rewritten uncompressed,wal_enabled = falseflipped back totrue.clawhdf5-agent:compression = truenever worked in a default build (it requested Zstd without enabling the feature, so every checkpoint failed withunsupported filter: 32015). Default builds now use deflate; Zstd is the new opt-inzstdfeature.clawhdf5-agent: single-writer lock (<store>.h5.lock,MemoryError::Locked) — two handles on one store used to silently destroy each other's data. NewHDF5Memory::open_read_onlygives a lock-free, never-writing view; the CLI's read-only subcommands use it.clawhdf5-agent: an unreadable WAL (torn header / bad magic) is quarantined (HDF5Memory::quarantined_wal()) instead of blockingopen()of a healthy store. A WAL from an unknown newer version still fails and is left intact.clawhdf5-agent: provenance records are renumbered on compaction (they weren't, so every latersave_or_updateraised a false High integrity alert); pending anomaly alerts and tracked sessions are bounded;snapshot()includes entries still in the WAL.clawhdf5-agent: hybrid ranking is deterministic (index tie-breaks instead ofHashMaporder); a set of identical positive scores — including a single candidate — normalises to 1.0 rather than 0.0; the Hebbian boost no longer reinforces zero-score filler results.clawhdf5-format: chunked/VDS/hyperslab reads size their buffers with overflow-checked arithmetic and fallible allocation, so crafted dimensions areFormatError::Overflowinstead of a wrapped size or a process abort;parallel_readbounds checks usechecked_add.clawhdf5: a malformed filter-pipeline message is an error instead of being treated as "no filters" (which returned compressed bytes as data);FileBuilder::writeis atomic and synced instead of truncating the destination first.
CI / Testing
- CI now lints every target (
cargo clippy --all-targets) plusclawhdf5-format's optional features, compiles all benches, and tests the format feature matrix. Previously test/bench code and feature-gated modules were never linted; the accumulated clippy backlog is fixed. - CI installs python3 + h5py/numpy/netCDF4/xarray and sets
CLAWHDF5_REQUIRE_INTEROP=1, which turns a missing interop dependency into a test failure. Until now every h5py/netCDF4 interop test silently skipped in CI, which is how the HDF5 2.0 compound bug fixed in v2.2.0 reached a user. The#[ignore]dwriter_h5py_testssuite is run explicitly. - h5py-generated-file tests now cover default libver bounds as well as
libver='latest'(HDF5 2.0 raised the default low bound to 1.8). clawhdf5-agent: WAL property tests (round trip; after any corruption the entries read back are an exact prefix of what was written — 1500 seeded cases), a crash-recovery matrix (an on-disk image after every operation, the checkpoint window, and the WAL torn at every byte length, each reopened and checked against a model), and a WAL fuzz target.- Optional fuzz smoke run (
CLAWHDF5_FUZZ_SECONDS=N scripts/ci-test.sh); new datatype corpus seeds for v1 compound and native complex messages.
v2.2.0 (2026-09-18)
Security
clawhdf5-format: bounded decompression output (MAX_DECOMPRESS_SIZE) for deflate/lz4/zstd/pcodec so a crafted compressed chunk can't drive an unbounded allocation (memory-exhaustion DoS).clawhdf5-format:chunked_read.rs/data_read.rs/local_heap.rsbounds audit — addedensure_lenoverflow guards at every plain-arithmetic offset+size check, a recursion-depth guard against a crafted self-referencing/cyclic B-tree chunk index, a fix for an unguarded compound-datatypebyte_offsetoverrun inread_compound_fields, and anndims - 1underflow guard for degenerate zero-dimension chunked layouts. Added a newfuzz_dataset_readcargo-fuzz target (walks every dataset in a parsed file and exercises the contiguous/chunked/compact raw-data read paths) which found and fixed 3 real crash bugs — an integer-multiply overflow incopy_chunk_to_output's N-D assembly path, thendims - 1underflow above, and an overflow inlocal_heap.rs— within the first few fuzzing runs.clawhdf5-format:btree_v1.rsoverflow-safe bounds checks via a localensure_lenhelper, closing ausize-overflow panic reachable from a crafted near-usize::MAXB-tree offset.clawhdf5-agent: WAL length-prefix caps (MAX_WAL_FIELD_LEN, 64 MiB) reject a corrupted/truncated length claim before allocating. Followed by a full per-entry CRC32 trailer (WAL_VERSIONbumped to 2) — a bit-flip inside an entry now stops replay cleanly instead of silently accepting corrupted data. Old-format WAL files are still read correctly and migrated to the new format on next open.clawhdf5-android: validateembedding_len/query_embedding_lenagainst the handle's configuredembedding_dim(and reject null pointers) before constructing a slice from a raw pointer inedgehdf5_save/edgehdf5_hybrid_search.clawhdf5-py: bump pyo3/numpy0.28→0.29, clearing two RUSTSEC advisories (OOB read inPyList/PyTupleiterator; missingSyncbound onPyCFunction::new_closure).- Clarified that the integrity hashes in
clawhdf5-agent::provenance(FNV-1a) andclawhdf5-format::provenance(SHA-256) are unkeyed and detect only accidental corruption, not tampering — doc-only change, no behavior change.
Performance
clawhdf5-format: chunk cache lookup is now O(1) (slot_index: HashMap) instead of a linear scan, and cache hits return a sharedArcinstead of cloning the decompressed buffer — the hottest path in chunked reads.clawhdf5-ann: optionalparallelfeature (rayon) parallelizes HNSW'sprune_connectionsneighbor-distance computation. The outer build/insert loop is deliberately left sequential — it has genuine cross-iteration data dependencies and needs its own correctness-focused design pass.clawhdf5-format/chunked_read.rs: removed 12 unnecessarychunk_dimensions[..rank].to_vec()allocations where callees already accept&[u32].
Architecture
- Added
.gitea/workflows/ci.yml, actually wiring the long-existingscripts/ci-test.sh(fmt, clippy, tests, no_std check) into CI on every push/PR tomain. Fixed stale package names inci-test.sh/check-nostd.shthat had been silently no-op'ing theclawhdf5-pyexclusion and the no_std check. - Fixed a genuine no_std build break in
clawhdf5-format(uncovered once the no_std CI check actually started running):core::sync::atomic::AtomicU64doesn't exist onthumbv7em-none-eabihf(switched toportable-atomic), missingallocimports forBox/Vec/format!on a few no_std paths, andf64::powi(std/libm-only) replaced with a local exponentiation-by-squaring helper in the scale-offset filter. - Added
[workspace.dependencies]fortempfile/criterion/half/serde, fixing a real version skew onhalf(2vs2.7across crates). - Fixed version skew:
clawhdf5-py(pyproject.toml) andpackages/clawhdf5-node(package.json) were both behind the actual crate version (2.1.0). - Documented that the
mpi-iofeature's read/write paths are root-read +broadcast / gather-to-rank-0, not true collective I/O.
Documentation
- BENCHMARKS.md: re-ran the previously-undated "LongMemEval Results", "SIMD & Parallelism", and "Vector Search Latency"/"Comparison to MemX" sections on a second machine (tank, Ryzen 7 7800X3D) with explicit dates and reproduce commands. Found and corrected a methodology issue in the SIMD/Parallelism benchmark selection (several originally-compared benchmarks didn't actually isolate the scalar/SIMD/parallel axis).
- README.md / ROADMAP.md / CLAUDE.md: corrected several stale facts —
the
clawhdf5-typescrate (removed earlier) was still listed in the README crate map; the LongMemEval numbers in the README badge and table didn't match the actual (much better) benchmark results in BENCHMARKS.md; total line-of-code and test-count figures were stale;clawhdf5-gpu's CubeCL→wgpu correction; documented the newclawhdf5-annparallelfeature flag, which had no entry in the Feature Flags table.
New Features
clawhdf5-migrate: substantial engine improvements:- Real content validation — the post-migration check now reads the written
HDF5 back and compares actual content (chunk text, embeddings, and every
session/entity/relation field) against the source, not just row counts. A
representative sample of chunk rows is verified by default;
--validate-fullchecks every row. A corrupt migration that preserves counts no longer passes. - Configurable schema — table names are no longer hardcoded; queries are
built from a
SchemaConfig(table + ordered column names, defaulting to the ZeroClaw layout) with--chunks-table/--sessions-table/--entities-table/--relations-tableoverrides. - Streaming count pass —
--dry-runnow does aCOUNT(*)-only pass per table instead of loading every row into memory. - Incremental migration —
--incrementalreads the existing output, reads only source chunks newer than the last migrated id, and appends them (refreshing the metadata groups), instead of re-migrating everything.
- Real content validation — the post-migration check now reads the written
HDF5 back and compares actual content (chunk text, embeddings, and every
session/entity/relation field) against the source, not just row counts. A
representative sample of chunk rows is verified by default;
clawhdf5-format: read IEEE-754 half-precision (f16) floats.read_as_f32/read_as_f64previously only handled 4- and 8-byte floats; 2-byte floats (e.g. float16-stored embeddings) now decode via a no_std-safe bit conversion.clawhdf5-format: write multi-block fractal heaps (root indirect block). Dense attribute and dense link storage previously capped at a single direct block (~64 KiB of heap data — a few thousand attributes/links). When the objects exceed one direct block, the heap now lays out a root indirect block (FHIB) over multiple direct blocks sized by the doubling table, distributing objects across blocks with correct per-block heap offsets. Validated end-to-end: a 2,500-attribute object and a 2,500-link group round-trip through our reader and are read correctly by h5py. (Objects still may not span a block — no huge-object path.)clawhdf5-format: write dense group link storage (fractal heap + v2 B-tree). A group with more than 8 links (libhdf5's compactmax_compactdefault) is now written densely — its links live in a fractal heap indexed by a v2 B-tree of type 5 (link-name index) referenced from the group's LinkInfo message — instead of as inline Link messages. This matches libhdf5's compact→dense switchover and keeps large groups out of the object header. Reverse-engineered against libhdf5: link heaps useheap_id_length7 /max_heap_size32 (vs 8 / 40 for attributes). The shared single-direct-block fractal-heap builder is now parameterized and used by both dense attributes and dense links. Validated end-to-end: our reader round-trips, and h5py reads the dense groups we write. (Single direct block — up to ~a couple thousand links per group; beyond that needs indirect blocks, still unsupported.)
Robustness
clawhdf5-format: harden the readers added this cycle against malformed / hostile input — they parse untrusted bytes and must return errors, never panic, OOM, or recurse without bound. Fixed concrete vectors found by audit and locked in with adversarial tests:- Paged Fixed Array:
1 << max_nelmts_bitsshift overflow (au8≥ 64); element/page offset multiplications now checked; element count bounded by file size. - H5S selection decoder:
ALL/NONEno longer claim 16 bytes they don't have; hyperslabrankcapped at 32 (H5S_MAX_RANK) to stop a giant allocation;iter_linearcoordinate/stride/product arithmetic is checked. - VDS mapping parser: no pre-allocation from the untrusted
nused; all selection slicing is bounds-checked. - scale-offset / N-Bit filters:
1 << minbitsoverflow atminbits == 64; N-Bitbit_offset + precisionoverflow; N-Bit type-tree recursion depth capped (no stack overflow from a crafted nested tree); element counts bounded by the chunk's expected decompressed size so a bogus count can't drive a huge allocation. - Virtual Dataset assembly: a virtual dataset whose source is itself virtual (a cycle) now errors instead of recursing into a stack overflow.
- Paged Fixed Array:
New Features
clawhdf5-agent: compress fixed-length string datasets (memory text chunks, session summaries, ids, tags, entity/relation names, …). These were always stored uncompressed with a "chunked compound not yet supported" note that was simply stale — chunked writes work for fixed-size string/compound datatypes like any other.write_string_datasetnow chunks + deflates a string dataset once its payload reaches 4 KiB, so large, highly-redundant NullPad content shrinks substantially while tiny metadata stays contiguous (no chunk-overhead bloat).clawhdf5-format: decode the scale-offset filter (id 6) — both the integer variant (H5Z_SO_INT) and the floating-point D-scale variant (H5Z_SO_FLOAT_DSCALE). Handles signed/unsigned int sizes, f32/f64, negative minima, decimal scale factors and fill values; reverse-engineered against HDF5 2.0 and validated end-to-end. The float E-scale variant remains unsupported.clawhdf5-format: decode the N-Bit filter (id 5) — atomic, compound and array layouts (the full recursive type tree, nestable to any depth), previously unsupported. Signed and unsigned reduced-precision integers and float members all read end-to-end, validated against HDF5 2.0.
New Features
clawhdf5/clawhdf5-format: read external-file Virtual Datasets (VDS). The format layer gainsread_raw_data_full_with_resolverand aVdsSourceResolvercallback (Fn(&str) -> Option<Vec<u8>>) that maps a stored source file name to its bytes, so the pure-byte reader can pull in external sources without a filesystem of its own. Theclawhdf5FileAPI wires a default resolver that reads sibling source files relative to the opened file's directory, soFile::open(...).dataset(...).read_*()now transparently assembles cross-file VDS. A source file the resolver cannot supply leaves its region at the fill value (matching HDF5); an external source with no resolver at all is a clean error. In-memory files (File::from_bytes) have no directory, so only same-file VDS resolves there.clawhdf5-format: assemble same-file Virtual Datasets (VDS) of any rank. Previously a virtual layout returnedUnsupportedVersion. The reader now decodes the global-heap mapping block (reverse-engineered against HDF5 2.0:version · nused · [source-file · source-dataset · source-selection · virtual-selection]* · checksum, including the block-version-1 same-file marker), decodes theH5Ssource/virtual dataspace selections (ALL, NONE, and version-3 regular hyperslabs), reads each same-file source dataset, and scatters its selected elements into the virtual buffer in row-major order (so multi-dimensional block mappings land correctly); unmapped regions are left at the zero fill value. External-file sources return a clean unsupported error. The previousparse_vds_mappingsused a guessed layout that did not match real files and is replaced.
Tests
clawhdf5-format: regression test for scale-offset float E-scale datasets. The HDF5 library does not implement E-scale encoding — when asked for it (cd_values[0] = 1) it stores the chunk raw and sets the chunk filter mask to skip the filter — so these files read back verbatim purely by honoring the per-chunk filter mask. The test locks in that behavior against a fixture produced via the HDF5 low-level API; no E-scale decoder is needed.
Bug Fixes
clawhdf5-format: read multi-direct-block fractal heaps. The reader split direct vs indirect block rows using the FRHP "Starting # of Rows in Root Indirect Block" field (a constant, typically 1), so any heap whose data spans more than one direct block — common in libhdf5 files with a large group or many dense attributes — was misread as having indirect blocks and failed withInvalidFractalHeapSignature. The split is now derived from the heap geometry (max_direct_rows = log2(max_direct / start) + 2). Validated against an h5py-written 400-dense-attribute group (root indirect block, 4 rows, 13 direct blocks).clawhdf5-format: scope the per-file chunk cache by dataset. The sharedChunkCachebuilt its chunk index once and reused it for every chunked dataset in the file, keyed only by chunk coordinate with no dataset discrimination. With a single chunked dataset per file this was latent; once a file holds two chunked datasets of different rank (e.g. a 1-D compressed string array and the 2-D embeddings matrix), the first dataset's index was reused for the second, panicking with an out-of-bounds chunk coordinate. The cache now rebinds (dropping its index, chunk-index map, layout, and decompressed slots) whenever the dataset being read changes, while still caching repeated/sequential access to the same dataset.clawhdf5-format: read paged Fixed Array chunk indexes. A filtered, fixed-dimension dataset with more than one data-block page (>1024 chunks by default) previously failed with "paged Fixed Array data blocks not yet supported". The reader now walks the page-init bitmap (MSB-first), skips uninitialized pages, and resolves each page's fixed full-size slot (including the short final page). Reverse-engineered and validated end-to-end against an HDF5 2.0 file.clawhdf5-format: read array-typed datatypes (e.g. an array-typed compound member) viaread_as_i32/i64/u64/f32/f64— previously aTypeMismatch. The array is read as a flat sequence of its base elements (recursing for nested arrays), applying base-type precision rules.clawhdf5-format: sign-extend reduced-precision fixed-point integers on read. A signed integer whose datatype precision is smaller than its storage size is stored zero-filled, so e.g. a 16-bit-precision-1previously read as65535. The integer read paths now extract the precision field and sign-extend (full-width types are unchanged). Completes signed N-Bit reads and also fixes un-filtered reduced-precision integer datasets.clawhdf5-format: read datasets written by modern HDF5 (1.14+/2.0, i.e.libver=latest). Compound (class 6) and array (class 10) datatype version 5 messages and data layout version 5 messages were rejected as invalid; they reuse the v3/v4 binary structure, so they are now accepted. This unblocks reading compound types and — critically — every chunked/compressed dataset written by HDF5 2.0. Found by running the h5py interop tests against h5py 3.16 / HDF5 2.0. Independently reported (with a patch) against the v2.1.0 tag by M. Scot Breitenfeld (The HDF Group) — v2.1.0 predates this fix.clawhdf5-format: parse HDF5 2.0 native complex datatypes (class 11, datatype version 5, e.g.H5T_COMPLEX_IEEE_F64LE). The properties are a single base floating-point datatype, not a compound-style member list; the old parser read the base type's bytes as member names, producing a garbage datatype, and failed withUnexpectedEofwhen a complex type was nested in a compound. It is now surfaced as the equivalent{r, i}compound (the shape h5py writes for numpy complex dtypes), with a size check against the base type. Validated end-to-end against an HDF5 2.0-written file.
Performance
clawhdf5-format: chunked writes now compress all chunks up front viacompress_all_chunks, running across rayon threads under theparallelfeature when there are more than 4 filtered chunks. On-disk layout is unchanged. Speeds up compressed embedding writes inclawhdf5-agent(which enablesparallel).
Documentation
- Fix stale package names across all 13 per-crate READMEs (
rustyhdf5-*/edgehdf5-*→clawhdf5-*, usage versions → 2.1.0). - Correct README workspace/test/crate stats and the CLAUDE.md CLI subcommand
list; document the
hnswand format compression/checksum feature flags and theentity_extract/async_memorymodules.
v2.1.0 (2026-06-03)
New Features
clawhdf5-agent: HNSW now backs the vector stage ofhybrid_search. Thehnswfeature is on by default, so semantic search uses the approximateclawhdf5-annindex instead of a linear cosine scan. The index mirrors the memory cache (node id == cache index) and self-heals — it rebuilds whenever it drifts from the cache length, so no mutation path can desync it. Non-indexable stores (no/zero-dim/mixed embeddings) and dimension-mismatched queries fall back to the exact linear scan. Disable with--no-default-features --features float16for exact search.clawhdf5-ann: HNSW is now a live, mutable index — addedinsert,mark_deleted(soft-delete bitset; deleted nodes are traversed for connectivity but never returned),compact(drops deleted vectors and renumbers survivors), andnew(empty index). Serialization gains a format version tag (HNSW_FORMAT_VERSION= 2) and persists the deleted bitset; pre-existing v1 files still load.clawhdf5-agent:hybrid::merge_vector_keywordexposes the shared normalize-and-fuse step used by both the linear and HNSW vector paths.- Expose
max_dimensions()API on Dataset, MmapDataset, and LazyDataset - NetCDF-4 unlimited dimension detection now works correctly
- Python bindings (
clawhdf5-py) build and link on macOS with system Python
Bug Fixes
clawhdf5-py: upgrade PyO3 and numpy0.23→0.28so the bindings build on Python 3.14 (PyO3 0.23 capped at 3.13 and hard-failedcargo build --workspace). Updated for the removedPyObjectalias (Py<PyAny>) and thePython::allow_threads→Python::detachrename.- Fix GPU L2 distance test (squared vs actual L2 mismatch in test helper)
- Mark Android JNI functions as
unsafefor Rust 2024 edition compliance - Add
# Safetydocumentation to all public unsafe extern functions - Fix all clippy warnings: needless_range_loop, manual_strip, ptr_arg, etc.
- Rename
RelationType::from_strtofrom_labelto avoid trait confusion - Isolate h5py interop tests with
#[ignore]when h5py unavailable
Code Quality
- Full rustfmt pass across workspace (61 files)
- Refine inner unsafe blocks for Rust 2024 edition style
- Zero clippy warnings, zero clippy errors across entire workspace
- 1,546 tests passing, 0 failures
v2.0.0 (2026-03-19)
- Unified rustyhdf5 (11 crates) and edgehdf5 (4 crates) into a single workspace
- All crates renamed to clawhdf5-* prefix
- Version bumped to 2.0.0 across all crates
- Git dependencies replaced with in-workspace path dependencies
- Added
agentfeature flag to clawhdf5-agent