Skip to content

v0.13.1

Choose a tag to compare

@github-actions github-actions released this 10 Aug 06:54
· 8 commits to develop since this release
4fb0c24

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_cleanup had no pinned filter. Pinned memories could be hard-deleted during a routine aging pass. (#974)
  • FTS5 search read rank from the wrong column offset, returning results in the wrong order. (#975)
  • Pinned memories got importance = 0.0 during recompute because the salience multiplier was zeroed out. (#976)
  • update() and update_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 with safe_truncate() using is_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 with serde_json::to_string(). (#981, #984)
  • Windows vector save had a TOCTOU race between exists() and write(). Fixed with OpenOptions create-only. (#982, #984)
  • Vector dimension arithmetic overflowed usize on very large indices. Switched to saturating_add. (#970)

Concurrency

  • consolidate() held a write lock through its entire loop. The lock is now scoped to the mutation, with save() 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 single SELECT ... WHERE id IN (...).
  • Hybrid search (4 call sites): called touch_access() per result. Now batches via touch_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

Contributors