* fix(memory): coerce stored confidence in the three remaining raw reads
`_coerce_source_confidence` exists because `memory.json` is user-editable and
written across versions: a `confidence` that is null, a str, a bool, out of
range, or non-finite still has to read back as a usable score. Consolidation
already routes every stored read through it, and #4023 did the same for the
max_facts trim. Three reads still take the field raw.
`_build_staleness_section` formats it with `f"{conf:.2f}"` — a str raises
ValueError, None raises TypeError. `_do_update_memory_sync`'s `except Exception`
swallows that into `return False`, aborting the whole memory-update cycle
permanently, since the offending fact is then never rewritten.
The staleness cap's `sort(key=lambda f: f.get("confidence", 0))` ranks the facts
it is about to delete. A str raises the same way; the values that don't raise
mis-rank instead. `true`/`inf` outrank a genuine 0.9 and push it into the removal
slot; an absent key ranks 0 and is deleted first; `nan` compares false against
everything, so which fact dies depends on where the corrupted one happens to sit
in the file.
`search_memory_facts` — the `memory_search` tool, added in #4023 — ranks results
the same raw way and then truncates to `limit`, so the same mis-ranking hands the
model the wrong facts, or fails the tool call outright on a str.
All three now read through the helper, so an unusable stored confidence ranks as
unknown (0.5): neither kept ahead of a real score nor evicted before one. The two
sorts run in opposite directions, and 0.5 is load-bearing in both.
* test(memory): anchor the confidence delta at every coerced read
The str-based tests raise on main, so they cannot go red for any stored
confidence that mis-ranks without raising. `true`/`false`/`inf` and an absent key
never reached the except handler at all: bool subclasses int, so `true` ranked
1.0; `inf` outranked every real score; an absent key ranked 0. Silent fact loss
in the staleness cap, wrong results out of `memory_search` — no log line either
way.
Parametrize both ranking sorts over the four non-raising inputs that fall to the
0.5 default, each pitted against a genuine neighbour chosen so the survivor
flips. The sorts run in opposite directions, so the one matrix covers eviction
from both ends.
`nan` is excluded from those: its old rank is undefined rather than pinned to an
end of the order, so one fixed input order happens to yield the correct survivor.
Its real property is order-independence, asserted across both fact orders.
The staleness prompt formatter is pinned by rendered value, not merely by not
raising: 1.5 → 1.00, -0.3 → 0.00, and inf/nan/true/None → 0.50, matching the
consolidation prompt that reads the same field through the same helper.