v0.13.1
v0.13.1
Patch release. Security fixes, data integrity corrections, and a significant query performance overhaul. No breaking changes.
What happened
The v0.13.0 lifecycle release shipped on August 8. A follow-up audit found 10 security and data integrity bugs in the codebase, ranging from command injection to a missing pinned-memory guard in the aging cleanup. This patch fixes all of them, plus eliminates N+1 query patterns that were scaling poorly on larger stores.
Security
- Command injection in the Hermes plugin extension. The URL parameter was passed unsanitized to a shell command. Fixed with proper argument escaping. (#973, #969)
- install.ps1 on Windows did not abort on checksum mismatch. A corrupted download would install silently. The installer now stops. (#980)
Data integrity
Four bugs that could lose or corrupt data:
aging_cleanuphad no pinned filter. Pinned memories could be hard-deleted during a routine aging pass. (#974)- FTS5 search read
rankfrom the wrong column offset, returning results in the wrong order. (#975) - Pinned memories got
importance = 0.0during recompute because the salience multiplier was zeroed out. (#976) update()andupdate_fields()wrote memory and tags in separate statements without a transaction. A crash between them left inconsistent state. Now wrapped in a single transaction. (#977)
Crash and race condition fixes
- UTF-8 panics from
&str[..N]slicing in 8+ locations. Replaced withsafe_truncate()usingis_char_boundary(). Zero unsafe blocks remain. (#978, #969) VectorIndex::build()trusted the first item's dimensions without checking. One bad vector corrupted the whole index. Now validated. (#979)- Chunker could infinite-loop when a chunk boundary landed inside a multi-byte character. (#969)
- Manual JSON construction via
format!()in 6 CLI locations. Replaced withserde_json::to_string(). (#981, #984) - Windows vector save had a TOCTOU race between
exists()andwrite(). Fixed withOpenOptionscreate-only. (#982, #984) - Vector dimension arithmetic overflowed
usizeon very large indices. Switched tosaturating_add. (#970)
Concurrency
consolidate()held a write lock through its entire loop. The lock is now scoped to the mutation, withsave()called once after. (#986)- Server thread limiter used a busy-spin
AtomicUsize. Replaced with a Condvar-based semaphore for proper sleep/wakeup. (#986) - Edge insertion and re-embed loops had no cap on concurrent operations. Now bounded to avoid exhausting the connection pool. (#985)
Query performance
The biggest change in this release. Four call sites were doing N individual SQL queries where one batch query would do:
- recall(): fetched each candidate memory with a separate
get_by_id(). Now batches into a singleSELECT ... WHERE id IN (...). - Hybrid search (4 call sites): called
touch_access()per result. Now batches viatouch_access_batch(). - Graph BFS: fetched frontier nodes and relationship targets one by one. Now batched per level.
- Room recall: fetched missing memories in a loop. Now a single
get_by_ids()call. - Edge insertion: called
tx.execute()per edge, re-preparing the statement each time. The prepared statement is now hoisted outside the loop.
On a store with 1000 candidate memories, recall previously ran 1000+ queries. Now it runs 1. The batch methods chunk IDs in groups of 900 to stay under SQLite's 999 host parameter limit.
Stats
| Metric | v0.13.0 | v0.13.1 |
|---|---|---|
| Tests | 430 | 476 |
| Clippy warnings | 0 | 0 |
| Unsafe blocks | 3 | 0 |
Dead code (pub fn) |
4 | 0 (deprecated) |
| Open issues | 10 | 0 |
Issues and PRs
Issues closed: #973, #974, #975, #976, #977, #978, #979, #980, #981, #982
PRs merged: #965, #966, #967, #968, #969, #970, #971, #972, #983, #984, #985, #986, #987, #988, #989