v2.4.9
Second slice of the 2026-04-19 audit fix wave. Ships the remaining HIGHs
plus six MEDIUMs covering scoring parity, retrieval-pipeline dead code,
federation input safety, and MCP-layer hygiene.
Fixed — scoring parity
_surprise_score_mcpwas a stale duplicate that missed the 2.2.3
neutral-fallback fix; the MCP write path — the dominant one in
practice — still returned(1.0, "fts5_no_matches")where
_impl._surprise_scorereturned(0.5, "fts5_no_matches_neutral").
Deleted the copy and imported the canonical scorer. Regression test
attests/test_mcp_surprise_score_parity.pylocks the parity.affect_log.safety_flagwas NULL on every row because
brain.affect_logread the wrong key from the classifier
(safety_flagsingular) whenclassify_affectreturns
safety_flags(plural list of pattern dicts). Now serializes the
list as JSON when any pattern fires; NULL otherwise — keeps the
idx_affect_safetysparse index honest.
Fixed — retrieval correctness
- The FTS-confidence relative-anchor gate was dead code. Entry
condition already established the relative-strong verdict, but the
block immediately reassignedrel_strongto the opposite relation.
Only absolute-threshold detection was running. graph_traversalintent produced zero-row expansion onevents
because the caller didtbl_key.rstrip("s")before passing to
_graph_expand, which queriesknowledge_edgeswith plural table
names. Now passes the canonical name directly.- Events/context silently cascaded into FTS-only when the memories
bucket tripped the_fts_strong_anchorgate. Added matching
_debug_skipsentries so operators can see the cascade. _reason_l1_search,cmd_push, andBrain.orient()now build
the same_build_fts_match_expression(sanitize(...))OR expression
cmd_searchuses, instead of bare sanitize. Multi-word queries no
longer fall into FTS5 implicit-AND semantics at those callsites.
Fixed — CLI / diagnostics
brainctl doctorhardcoded"ok": Trueand exited 0 regardless
of health. JSON output now reflects the actual verdict
(ok == healthy), and the process exits 1 when any issue is
present — shell$?-based automation now works correctly.- Plugin env-var mismatch between
BRAINCTL_DBandBRAIN_DB.
paths.get_db_path()now honorsBRAINCTL_DBas the go-forward
canonical name (matches theBRAINCTL_HOME/_BLOBS_DIR/
_BACKUPS_DIRfamily) while keepingBRAIN_DBworking as a
deprecated alias.BRAINCTL_DBwins when both are set.
Fixed — federation / merge / MCP shape
- Federation LIKE fallbacks didn't escape
%/_in the four
LIKE query sites acrossfederated_memory_search,
federated_entity_search, andfederated_search. Introduced
_escape_like+ESCAPE '\\'on every LIKE clause soapi_key
matches literally, notapikeyorapi-key. - Self-merge surfaced as
"database is locked". Added an
explicitsource == targetguard (resolved to absolute path) that
raises a clearValueErrorbeforeATTACH DATABASEruns. - MCP error shape normalized across
mcp_serverand
mcp_tools_knowledge: every error return now carries
{"ok": False, "error": ...}instead of a bare{"error": ...}.
Clients that branch onokno longer need per-tool special-cases.
Fixed — packaging
- Three MCP tools depended on
~/bin/lib/drop-ins
(belief_revision,outcome_eval,quantum_retrieval) — they were
unreachable on PyPI installs. Moved all three in-tree under
agentmemory.lib.*; imports prefer the in-tree copy and fall back
to the legacy path for users on the old layout.
Testing
1866 passed, 28 skipped, 2 xfailed locally (3 new tests vs 2.4.8
for the surprise-score parity regression).