Skip to content

Python: relevance-score gates (min_relevance_score / DEFAULT_RELEVANCE) have no polarity cases in tests: a memory that says the opposite of the query is recalled as highly relevant. #14295

Open

Description

Language: Python. The .NET defaults differ and are recorded at the end.
Verified against main at b39d95a on 2026-08-25.

Hi SK team. I just finished a construct-validity audit of embedding-cosine gates across agent
frameworks, and SK's memory-recall path carries the same test blind spot I found in four other
suites. Preprint: https://arxiv.org/abs/2608.10216.

The pattern. TextMemoryPlugin.recall gates on DEFAULT_RELEVANCE = 0.75
(text_memory_plugin.py#L22),
and SemanticTextMemory.search gates on min_relevance_score, defaulting to 0.0
(semantic_text_memory.py#L129),
which it passes through to the store at L153. Both implement one decision, "is this stored text
relevant to, meaning does it answer or agree with, this query?", and both answer it by thresholding
embedding similarity. That score measures shared wording rather than shared meaning, and in the pairs
a recall gate most needs to rank correctly the two run opposite ways: reversing an instruction is a
one-token edit that keeps similarity near maximal, while a faithful restatement in fresh words drops
it substantially. To be precise about framing, nothing here is a bug in SK's code, and the gate does
exactly what it says. The blind spot sits in what the test suite asserts the gate means.

Specimen from my audit corpus (cosine under nomic-embed-text-v1.5, MRL-256, frozen in the paper's
artifact; the mutation class scored 0.83 to 0.9997 across all nine encoders tested, so this is not
one model's quirk):

  • Stored memory: "Withhold the study drug from any participant who reports chest tightness."
  • Query: "Administer the study drug to any participant who reports chest tightness."
  • Cosine 0.9608. It clears 0.75 comfortably, and clears any plausible relevance floor. At the 0.0
    default it is not even a speed bump.
  • Mirror case: faithful low-overlap restatements in the same corpus average about 0.76 under the same
    encoder, for example "Retry the request at most three times." against "Give the call up to three
    attempts, then stop.". That average lands right at the plugin cut, so faithful restatements sit on
    the knife edge while reversals clear it comfortably.

What the suite covers today. recall has no behavioral unit test. Across python/tests,
TextMemoryPlugin appears only in test_serialization.py, and the one test that exercises the gate
is
test_embedding_service_with_memory.py#L155-L169,
an integration test needing live credentials, which calls memory.search(...) four times and passes
min_relevance_score=0.0 at every call site. The threshold is never exercised at a non-zero value
anywhere in the suite, and the fixtures are taxonomic facts, so no pair shares wording while
disagreeing about what it decides.

Proposal, sized to be PR-able, and I am happy to write it:

  1. Add polarity cases: pairs sharing wording with opposite decisions (negation, must to may,
    quantity changes, scope inversion) asserted at the shipped defaults, and pairs sharing the
    decision with no shared wording. Section 11.3 of the paper gives the two families. The corpus
    pairs are released and were authored blind to any encoder.
  2. Record the vectors from a named encoder and check them in with provenance (model id, dimension,
    date, generating script) rather than hand-authoring them. A hand-authored vector asserts only the
    fixture author's arithmetic, and passes whatever the sentences happen to say.
  3. Optionally, one line in the relevance and min_relevance_score docstrings noting that the score
    is surface-form similarity, so "relevant" should not be read as "in agreement".

.NET, and a correction to my original filing. @journaltraces checked the C# side and found two things
I had wrong.
TextMemoryPlugin.cs#L47
has private const double DefaultRelevance = 0.0;, so the .NET plugin ships an open gate where the
Python one ships 0.75. And VectorStoreTextSearch carries no minRelevanceScore at all:
ExecuteVectorSearchCoreAsync builds a VectorSearchOptions<TRecord> with Filter only and passes
top. I originally wrote that the deprecation migration ports the blind spot forward. It does not
port it, it drops the gate: a caller who migrates moves from a threshold that can at least be raised
to a top-k that always returns its k, with no floor left to argue about. Thanks to them for both.

The audit found the identical gap in LlamaIndex, LangChain, and GPTCache-style caches. SK is in good
company, which is rather the point: the failure mode ships because no framework's tests can see it.

Activity

  1. DariusHQ commented on Aug 17, 2026

    @DariusHQ

    Hi Scott, I took a look at the current C# main implementation before attempting a PR, and noticed a couple of differences from the current issue description.
    In the current TextMemoryPlugin, DefaultRelevance is 0.0, not 0.75, while SemanticTextMemory.SearchAsync does pass minRelevanceScore through to IMemoryStore.

    I also couldn't find an equivalent relevance-score gate in the current VectorStoreTextSearch successor path, it appears to use VectorSearchOptions<TRecord> and vector-store search without the same minRelevanceScore parameter

    I may be looking at a newer/different path than the one used for the audit. Could you clarify which current C# implementation/commit the DEFAULT_RELEVANCE = 0.75 and collection.as_text_search references are targeting?

    I wanted to confirm the intended path before preparing a PR so that the tests match the current SK implementation.

  2. poeticize commented on Aug 25, 2026

    @poeticize
    Author

    Darius,

    Thanks for actually reading the code before writing the PR, and sorry for the slow reply.

    You are looking at the right code. I pointed you at the wrong language: the audit ran against the
    Python implementation, and DEFAULT_RELEVANCE and collection.as_text_search are Python
    identifiers. The issue body never said so, which is my fault, and I have fixed it. Re-verified
    against main at b39d95a today:

    Your C# readings are both correct.
    TextMemoryPlugin.cs#L47
    has private const double DefaultRelevance = 0.0;, so the .NET plugin ships an open gate where the
    Python one ships 0.75. I will add it that divergence in the issue.

    Your VectorStoreTextSearch observation is the more interesting one, and strengthens the
    case. There is no minRelevanceScore there: ExecuteVectorSearchCoreAsync builds a VectorSearchOptions<TRecord> carrying Filter only, and passes top. So the successor path does not carry the score gate forward, it drops it. A caller who migrates off TextMemoryPlugin moves from a threshold that can at least be raised to a top-k that always returns its k, and the "administer the study drug" record comes back as the top hit with no floor left to argue about. My issue said the migration ports the blind spot forward. That was too generous, and I would rather state it your way.

    On where the Python gap actually sits, my original wording was loose. recall has no behavioral unit
    test at all. Across python/tests, TextMemoryPlugin appears only in test_serialization.py, and
    the one test that exercises the gate is
    test_embedding_service_with_memory.py#L155-L169,
    an integration test needing live credentials, which calls memory.search(...) four times and passes
    min_relevance_score=0.0 at every call site. The threshold is never exercised at a non-zero value
    anywhere in the suite, and the fixtures are taxonomic facts (Sharks are fish, Penguins are birds), so
    no pair in the suite shares wording while disagreeing about what it decides.

    One caution if you do write the PR, because there is already an open one against this issue (#14319)
    that ran into it. A unit test cannot call an embedding model, so the tempting move is to hand-author
    the vectors, and a hand-authored vector asserts only the fixture author's arithmetic. The test then
    passes whatever the sentences say, which is the failure this issue is about, reproduced one level up.
    What I think does work:

    1. Record real vectors once from a named encoder for the four sentences and check them in, with
      provenance in the fixture (model id, dimension, date, generating script). The assertion then says
      something about the world: the opposite-meaning record clears the shipped default while the
      faithful paraphrase does not. It stays frozen and deterministic, and it fails if the claim stops
      being true.
    2. Prefer an encoder the ecosystem actually runs, such as text-embedding-3-small, over the one in
      my paper. The effect does not hinge on the choice; the mutation class scored 0.83 to 0.9997 across
      all nine encoders I tested.
    3. Keep any live-encoder assertion in the integration project behind its existing trait, not in the
      unit tests.

    I am happy to generate the recorded vectors and hand them over so you keep authorship of the PR, in
    Python or C# or both. Say which and I will produce them.

  3. changed the title [-]Relevance-score gates (min_relevance_score / DEFAULT_RELEVANCE) have no polarity cases in tests: a memory that says the opposite of the query is recalled as highly relevant.[/-] [+]Python: relevance-score gates (min_relevance_score / DEFAULT_RELEVANCE) have no polarity cases in tests: a memory that says the opposite of the query is recalled as highly relevant.[/+] on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions