|
3 | 3 | All notable changes to Perseus Vault (formerly Mimir/Mneme) are documented here. This project adheres to |
4 | 4 | [Semantic Versioning](https://semver.org/). |
5 | 5 |
|
6 | | -## [Unreleased] |
| 6 | +## [2.14.0] - 2026-07-02 |
7 | 7 |
|
8 | 8 | ### Added |
9 | 9 | - Recall-first context injection (#366): `mimir_context` / `prepare` default |
@@ -46,6 +46,62 @@ All notable changes to Perseus Vault (formerly Mimir/Mneme) are documented here. |
46 | 46 | count 53 → 55. |
47 | 47 |
|
48 | 48 | ### Fixed |
| 49 | +- Pool-exhaustion collapse under concurrency (#397): recall, insert, and |
| 50 | + auto-embed each drew a SECOND pooled connection while holding one, so at |
| 51 | + ≥ pool-size concurrent requests every slot was held by a frame blocking on |
| 52 | + the nested draw — 32 clients vs pool 16 measured 174 req/s with 30-second |
| 53 | + stalls and failed writes; 64 clients wedged. `apply_recall_side_effects`, |
| 54 | + `find_near_duplicate`, and `store_embedding` now reuse the caller's held |
| 55 | + connection (`_with_conn` variants), including `mimir_embed`'s single-entity |
| 56 | + path; the same load now runs at ~4,200 req/s with zero errors. r2d2's |
| 57 | + checkout timeout is tunable via `MIMIR_POOL_TIMEOUT_MS`. A new |
| 58 | + `concurrency-gate` CI workflow pins the load test at 2× pool |
| 59 | + oversubscription plus the four concurrency hammer tests. |
| 60 | +- decay_tick write amplification (#399): every tick rewrote every |
| 61 | + non-archived row even when nothing changed — 412MB of WAL per tick on a |
| 62 | + 45MB database at 100k entities. Writes are now skipped when the recomputed |
| 63 | + score is within epsilon of the stored value (archive and layer-boundary |
| 64 | + crossings always write), so steady-state ticks write ~zero rows; |
| 65 | + `entities_updated` now reports rows actually written. |
| 66 | +- `mimir_history` pagination (#403): the tool returned every full decrypted |
| 67 | + version body with no limit — a hot key with 10k versions produced a |
| 68 | + ~10-15MB tool response. Now takes `limit` (default 20, newest-first, |
| 69 | + 0 = count-only) and `offset`, and reports `total`/`returned`. |
| 70 | +- follow() cross-workspace efficacy clobber (#391) and lost updates (#385): |
| 71 | + the key-addressed UPDATE stamped one workspace's counts and |
| 72 | + `efficacy_status` onto every (category,key) row — other workspaces and |
| 73 | + archived rows included — and the unlocked read-modify-write lost |
| 74 | + increments under concurrent calls. follow() now resolves ONE live row |
| 75 | + (the deterministic get_entity pick) under the audited writer lock and pins |
| 76 | + its UPDATE to that id. |
| 77 | +- link/unlink pool starvation (#387): both resolved the source entity via |
| 78 | + `get_entity()`, drawing a second pooled connection while one was held — |
| 79 | + ≥16 concurrent linkers hit 30s r2d2 timeouts with opaque `Error(None)`. |
| 80 | + Ids now resolve on the caller's own connection. |
| 81 | +- cohere error-path transaction leak (#388, corrected premise): the raw |
| 82 | + `BEGIN IMMEDIATE`/`COMMIT` pair had no rollback guard — any error between |
| 83 | + them returned the pooled connection with the transaction still open, |
| 84 | + permanently poisoning that slot ("cannot start a transaction within a |
| 85 | + transaction" on every subsequent checkout). cohere now uses the drop-safe |
| 86 | + transaction; errors roll back. (The filed links-clobber scenario could not |
| 87 | + occur — the pair-scan read already ran inside the writer transaction.) |
| 88 | +- remember() erased link graphs (#382): the MCP remember tool constructs |
| 89 | + entities with empty links and remember's full-row UPDATE wrote them |
| 90 | + wholesale — ANY re-remember of a linked entity deterministically erased |
| 91 | + its edges, and concurrent `mimir_link` calls could lose edges to the |
| 92 | + unguarded read-modify-write. link/unlink now run under the writer lock and |
| 93 | + remember UNIONS caller links with stored links (dedup by target; |
| 94 | + stored relationship/weight win; `mimir_unlink` is the only removal path). |
| 95 | +- invalidate_entity temporal-window corruption (#381): the fourth |
| 96 | + `entity_history` writer stamped `invalidated_at = now()` raw — an audited |
| 97 | + writer that legitimately set `recorded_at` ahead of the wall clock produced |
| 98 | + an INVERTED window, and a same-millisecond create+invalidate produced a |
| 99 | + zero-width window that `mimir_as_of` could never reconstruct. It now takes |
| 100 | + the writer lock and bumps `invalidated_at` strictly past `recorded_at`. |
| 101 | +- rekey-aad stale overwrite (#386): a `remember()` landing between rekey's |
| 102 | + read and its re-encrypted write was silently reverted to stale content |
| 103 | + under a valid ciphertext; the per-row write is now guarded on the |
| 104 | + ciphertext being unchanged. |
49 | 105 | - Audited-writer TOCTOU (#379): the three audited temporal writers (the #371 |
50 | 106 | re-assert path in remember, the #373 `set_valid_to` close, the #377 status |
51 | 107 | flip) read their preconditions on the bare pooled connection before opening |
|
0 commit comments