Skip to content

fix(memory): keep a zero confidence from scoring as the default - #5586

Merged
WillemJiang merged 1 commit into
bytedance:mainfrom
Lesereingrape:fix/zero-confidence-retrieval-weight
Sep 24, 2026
Merged

WillemJiang merged 1 commit into
bytedance:mainfrom
Lesereingrape:fix/zero-confidence-retrieval-weight

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5585

Why

A confidence of 0.0 is a value the storage layer validates, persists and considers legal
(0 <= confidence <= 1, deermem/core/storage.py:211-214), but two read sites rescued their
default with or 0.5, which cannot tell "explicitly zero" from "unset"
(deermem/core/retrieval.py:585, deermem/core/storage.py:1738). Reproduced as #5585:
BM25 × time_decay + confidence × 0.2 therefore added the default weight to a zero-confidence
fact, and the same stored fact scored 0.100001 when indexed through FTS5RetrievalAdapter.upsert
against 1e-06 when the identical row was indexed through FTS5Retrieval.index_fact — whose own
defaulting (fact.get("confidence", 0.5), retrieval.py:167 and :296) distinguishes the two
cases correctly. So the ranking of a discounted memory depended on which code path happened to
write its row, and the returned payload still reported "confidence": 0.0
(fact.setdefault at retrieval.py:459 never overwrites fact_json), which is why the inflation
is invisible to the caller. _compute_final_score itself is already strict in the right way: it
substitutes 0.5 only for a non-numeric or non-finite value (retrieval.py:503-507), so it
expects 0.0 to be a real weight.

What changed

  • A fact persisted with confidence: 0.0 now ranks strictly below one with 0.5, through both
    the FTS5 index and the substring fallback, and matches what the same fact already scores via
    FTS5Retrieval.index_fact. Callers see one consistent ranking regardless of which write path
    produced the row.
  • An absent or null confidence still means "unset" and still defaults to 0.5; the explicit
    null case keeps its existing tolerance instead of becoming a TypeError.
  • confidence: 0 in the substring fallback now returns a score of 0.0 rather than 0.5, so it
    can no longer outrank a mid-confidence fact when the index is unavailable or a scope is dirty.
  • Two tests appended to backend/tests/test_memory_retrieval_adapter.py, one per read path, each
    with an in-file control that pins the unchanged defaulting.

Surface area

FTS5RetrievalAdapter._document and FileMemoryStorage._search_substring. No schema change, no new
dependency, no I/O, and the stored Markdown payload is untouched — only the score derived from it.

  • Frontend UI
  • Backend API — memory search scores returned by search_facts
  • Agents / LangGraph — DeerMem retrieval ranking
  • Sandbox
  • Skills
  • Dependencies
  • Default behavior change — for facts whose stored confidence is falsy but legal (0, 0.0), the retrieval score drops by 0.1. Every other value, including a missing key, produces byte-identical scores to before, so no currently-ranking fact changes except the ones this report calls a bug.
  • Docs / tests / CI only

Bug fix verification

run tests/test_memory_retrieval_adapter.py
main @ 2ff006b0 + new tests (source reverted) 2 failed, 18 passed, 1 skipped
this branch 20 passed, 1 skipped

Both failures are the falsy collapse, verbatim:

FAILED test_indexed_zero_confidence_keeps_its_retrieval_weight  - assert 0.100001 < 0.100001
FAILED test_substring_fallback_keeps_zero_confidence_weight     - assert {'half': 0.5, 'zero': 0.5} == {'half': 0.5, 'zero': 0.0}

The controls pass on both sides, which is the point: test_indexed_zero_confidence_keeps_its_retrieval_weight
also asserts that a fact with no confidence key still scores exactly like an explicit 0.5, and the
18 pre-existing tests in the file are unaffected.

Validation

  • Not run to completion: PYTHONPATH=. pytest -m "not live" --ignore=tests/blocking_io tests/ (the
    make test suite) — the run on this branch was still in progress when this PR was written, so only
    the targeted 11-file set above is reported.
  • The 11 test files that import the storage/retrieval modules
    (test_memory_storage*.py, test_memory_retrieval_adapter.py, test_deermem_self_contained.py,
    test_memory_eviction.py, test_memory_fact_dedup.py, test_memory_scope_gate.py,
    test_memory_updater.py, test_updater_watermark.py, test_custom_agent.py):
    384 passed, 1 skipped on main → 386 passed, 1 skipped here (the +2 are the new tests; no
    existing test changes outcome).
  • The make lint steps, run directly: ruff check . → All checks passed!, and
    ruff format --check . → 1545 files already formatted (ruff 0.15.12, backend/ruff.toml).
  • Not run: make itself (no make on this Windows host, so the targets were executed as their
    underlying commands with the project venv), the live and blocking_io pytest marks, the frontend
    suites, and Docker e2e. Reported from a local source checkout, not CI.

AI assistance

Tool(s) used: Qoder (agentic CLI).

How you used it: reproduction scripting and the first draft of the two tests; the root-cause
call, the choice of None-only defaulting over the repo's .get(k, 0.5) idiom (so an explicit
null keeps its current tolerance instead of raising), and the line-by-line review of the final
diff were done here by the agent against main; no human has read the diff yet, which is why the box below is unticked.

  • The account owner should tick this once they have read the diff themselves.

or 0.5 cannot distinguish an explicit 0.0 from an unset field, so a discounted fact got the default confidence weight in both the FTS5 document and the substring fallback, while the same row indexed through FTS5Retrieval.index_fact kept its zero weight.
@Lesereingrape Lesereingrape changed the title probe (do not merge) fix(memory): keep a zero confidence from scoring as the default Sep 19, 2026
@Lesereingrape

Copy link
Copy Markdown
Contributor Author

Filed from #5585. This opened as a draft because the repository force-drafts pull requests from contributors without write access; nothing here is still in progress — the fix and both tests are complete and the validation numbers are in the description. A maintainer marking it ready is all that is needed to get it into review.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at f8d31e4 — no issues found; the fix is correct and minimal.

Verified: (1) 0.5 if confidence is None else float(confidence) at both read sites matches the storage contract — _normalize_fact (storage.py:211-214) persists 0.0 as legal and rejects non-numeric values at the write boundary, so only unset/null can reach the default here, and the explicit-null branch preserves the old or 0.5 tolerance. (2) A sweep of the deermem package found no remaining falsy-collapse confidence defaulting — FTS5Retrieval.index_fact / rebuild_from_facts already used the key-missing .get(k, 0.5) form, and _compute_final_score substitutes 0.5 only for non-finite/non-numeric values. (3) Rows persisted by the old code carry the inflated 0.5, but the index is derived data: the Gateway warm-up / lazy per-scope rebuild re-derives documents from canonical Markdown, so the correction takes effect on restart without a migration. (4) Ran tests/test_memory_retrieval_adapter.py at this head against the PR source: 20 passed, 1 skipped, matching the PR's reported results; both new tests pin the fix (zero < half) and the unchanged missing-key defaulting.

@Lesereingrape
Lesereingrape marked this pull request as ready for review September 24, 2026 00:53
@github-actions github-actions Bot added area:agents Agents, subagents, graph wiring, prompts, langgraph.json needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines labels Sep 24, 2026
@WillemJiang WillemJiang added this to the 2.2.0 milestone Sep 24, 2026
@WillemJiang
WillemJiang merged commit c77e874 into bytedance:main Sep 24, 2026
28 checks passed
hawkli-1994 pushed a commit to rongxinzy/deer-flow that referenced this pull request Sep 27, 2026
…dance#5586)

or 0.5 cannot distinguish an explicit 0.0 from an unset field, so a discounted fact got the default confidence weight in both the FTS5 document and the substring fallback, while the same row indexed through FTS5Retrieval.index_fact kept its zero weight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agents Agents, subagents, graph wiring, prompts, langgraph.json needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] A fact stored with confidence 0.0 is ranked as if it had the 0.5 default

3 participants