Skip to content

Faithfulness check before memory write , interested in contributing a pluggable verification hook #7283

Open

Description

@DataAlchmesit

Component

Python SDK

Description

I've been building a local faithfulness-checking library (MiniEval - (https://github.com/DataAlchmesit/MiniEval-pro), MIT licensed, 'pip install minieval-pro' and ran into a failure mode I think is worth raising here.

The problem: When an NLI-style faithfulness model checks a candidate fact against its source, it has no notion of whose fact it is. Given a source like "my brother is a lawyer" and a candidate "the user is a lawyer," a faithfulness model can score that as ~99% entailed. The professions match, so it entails, even though the fact is misattributed. I've tested this specifically and built two guards against it (possessive/reported-speech detection, and a relatedness check for cases where an unrelated pair gets forced into a false "contradiction" by the NLI model).

I looked through mem0/memory/main.py to understand where this would actually apply, and found the extraction happens in '_add_to_vector_store', a single LLM call parses extracted_memories from the conversation (around the "V3 PHASED BATCH PIPELINE" section), which then go through hashing/dedup and batch insert. That gap between "extraction parsed" and "batch persist" looks like the natural place a verification step could sit, but I want to ask rather than assume:

Is there already a supported way to hook into or intercept the extraction result before it's persisted , a callback, a config option, an extension point I'm missing?
If not, would a maintainer be open to discussing what a minimal, opt-in verification hook might look like? I'd rather design it with your input than build something against a private method (_add_to_vector_store) that could break on your next refactor.

To be clear about scope: I'm not proposing MiniEval become a dependency or that Mem0 adopt any particular faithfulness model. I'm asking whether an extension point for this class of check (verify before persist) is something you'd want, and if so, what shape it should take on your side.

Happy to share the specific test cases I've built (including ones where my own approach still fails, documented honestly) if useful context for the discussion.

AI Assistance

No AI involved, this is a need I hit myself

Activity

  1. modelpath-dev commented on Sep 11, 2026

    @modelpath-dev

    I will take this issue. Please assign it to me.

    The enhancement involves adding a verification hook in the Python SDK before writing to memory. The function _add_to_vector_store in mem0/memory/main.py seems to be the right place for this. I will inspect how extraction results are currently processed and propose a small, pluggable hook to verify data before it is persisted. I will ensure the change is minimal and opt-in.

  2. DataAlchmesit commented on Sep 11, 2026

    @DataAlchmesit
    Author

    I will take this issue. Please assign it to me.

    The enhancement involves adding a verification hook in the Python SDK before writing to memory. The function _add_to_vector_store in mem0/memory/main.py seems to be the right place for this. I will inspect how extraction results are currently processed and propose a small, pluggable hook to verify data before it is persisted. I will ensure the change is minimal and opt-in.

    Thanks for offering to help, @modelpath-dev! I'm actually already working on the PR implementation and design for this verification hook. I'll share a draft PR shortly and would welcome your review/feedback on it!

  3. sattyamjjain commented on Sep 12, 2026

    @sattyamjjain

    Not after the issue; you have the PR. One data point for the design.

    The misattribution case you hit is the write-side half of something that got measured last week. arXiv:2609.08258 tested five agent-memory systems and found none of them enforce revocation at retrieval: a fact marked invalid still comes back, still outranks its replacement, and still drives the action. https://arxiv.org/abs/2609.08258

    Same root cause as yours. A memory record carries no enforceable statement about where it came from or who it is about, so both the write check and the retrieval check are guessing from text. If the hook only validates entailment at write time, the retrieval half stays open.

    What worked for me was making provenance a typed field on the record instead of a boundary check. Who wrote it, under what capability, in what session- all part of the row and revocable by the principal afterwards. Then the write hook is one consumer of that field rather than the only place it is enforced, and "my brother" versus "the user" is a subject attribute you can test against, not an entailment score you have to trust.

    Design notes, if useful: https://github.com/sattyamjjain/mnemo. The opt-in hook at _add_to_vector_store still sounds like the right place to me.

  4. DataAlchmesit commented on Sep 12, 2026

    @DataAlchmesit
    Author

    Not after the issue; you have the PR. One data point for the design.

    The misattribution case you hit is the write-side half of something that got measured last week. arXiv:2609.08258 tested five agent-memory systems and found none of them enforce revocation at retrieval: a fact marked invalid still comes back, still outranks its replacement, and still drives the action. https://arxiv.org/abs/2609.08258

    Same root cause as yours. A memory record carries no enforceable statement about where it came from or who it is about, so both the write check and the retrieval check are guessing from text. If the hook only validates entailment at write time, the retrieval half stays open.

    What worked for me was making provenance a typed field on the record instead of a boundary check. Who wrote it, under what capability, in what session- all part of the row and revocable by the principal afterwards. Then the write hook is one consumer of that field rather than the only place it is enforced, and "my brother" versus "the user" is a subject attribute you can test against, not an entailment score you have to trust.

    Design notes, if useful: https://github.com/sattyamjjain/mnemo. The opt-in hook at _add_to_vector_store still sounds like the right place to me.

    @sattyamjjain Thank you for the incredibly timely reference to arXiv:2609.08258, this paper perfectly frames the exact retrieval-time vulnerability that soft revocation creates. You are spot on: if a memory system relies solely on raw text embeddings for similarity matching, the vector overlap will routinely override status tags during retrieval, leaving the system blind to who a fact belongs to or whether it was superseded.

    Your work on mnemo with typed, cryptographically accountable provenance fields is a fantastic reference design for this problem.

    To be clear on approach: While a robust read-side typed metadata solution (like mnemo's) is the long-term ideal for a complete engine overhaul, an opt-in hook in Mem0's _add_to_vector_store provides immediate protection against extraction hallucinations right now.

    From an architectural standpoint, an ingestion-time callback around _add_to_vector_store serves as a practical extension point to address both angles without forcing a massive, immediate overhaul of Mem0's database schema:

    1. Immediate Filtration: It allows local guards to completely drop clear upstream extraction hallucinations before they ever hit the database.

    2. Metadata Enrichment: Instead of just returning a binary pass/fail, the hook could allow developers to append the exact typed metadata fields you highlighted (e.g., explicit provenance_subject or session_owner tracking tags) directly onto the batch insert array.

    By exposing this opt-in hook, Mem0 gives developers the flexibility to enforce strict write-time hygiene and build the structural payload necessary to run robust retrieval-time filters.

    Would love to hear if the core maintainers think an architectural hook at this layer could serve as a useful bridge for these kinds of implementations.

  5. DataAlchmesit commented on Sep 18, 2026

    @DataAlchmesit
    Author

    Following up in case this got buried, since I did some concrete digging in the meantime that's relevant to the design.

    I traced the actual extraction schema and confirmed 'extracted_memories' (and the entity-linking phase in Phase 7) both operate on the whole episode text, there's no per-fact source span anywhere in the pipeline. I also confirmed the entity store (extract_entities/_link_entities_for_memory) does NER-style entity linking for search-boost ranking, which is a different problem from what I'm proposing . It doesn't check whether a fact is correctly attributed to the right person, just what entities a memory text mentions.

    That confirms the gap is real, but it also surfaced a second, more specific issue worth flagging: a whole-episode source means any per-fact faithfulness check has to deal with unrelated facts extracted from the same conversation contaminating each other's attribution signal. I tested this directly, running a batch of facts from one multi-topic episode through a faithfulness check, a third-party mention anywhere in the episode (e.g. "my brother is a lawyer") caused unrelated facts from the same episode to get incorrectly flagged too, because there's no way to isolate which sentence a given fact actually came from.

    So the real design question is narrower than my original post: is there any part of the pipeline where a fact retains a link back to the specific message(s) it was extracted from, even informally or would a verification hook necessarily have to work with the same whole-episode granularity the rest of the pipeline uses?

    Happy to share the actual reproduction if useful.

  6. sattyamjjain commented on Sep 19, 2026

    @sattyamjjain

    Answering the narrow question, since it is checkable in the file.

    On the infer=False path, the link exists. Each message becomes its own row and carries role,m plus actor_id taken from message_dict["name"] (main.py:894-903). Per-message provenance is right there.

    On the inferred path, it gets dropped in three specific places. parse_messages flattens the episode to a single string at:921, and that is what the extraction prompt receives at:950. At:992, only m.get("text") is read off each extracted memory, so any other field the model emitted is discarded there. And at:1029 the payload is deepcopy(metadata), the call-level metadata, not the per-message dict built at the top of the same function.

    So the answer is no on the path you care about. The useful part is that it is not a schema problem. role and actor_id are already valid payload keys; they sit in _IDENTITY_KEYS at:140 and in the allowed-key lists at:1228,:1346, and:1694, and the uninferred path already writes them. A per-fact source_role or source_actor_id would be carrying an existing key through a path that currently drops it, not adding a new one.

    One caution on what that actually buys. actor_id names who spoke, not who the fact is about. For "my brother is a lawyer," the speaker is still the user, so it does not settle your misattribution case by itself. It hands you the speaker half, which is the part your possessive detector currently has to infer from text.

    If you want the per-fact span rather than the speaker, the cheapest shape is having the extraction prompt emit a message index alongside text, then reading it at:992 and:1015 and carrying it at:1029. That is a small change, and it survives a refactor better than anything that reconstructs the mapping afterwards.

  7. DataAlchmesit commented on Sep 20, 2026

    @DataAlchmesit
    Author

    Answering the narrow question, since it is checkable in the file.

    On the infer=False path, the link exists. Each message becomes its own row and carries role,m plus actor_id taken from message_dict["name"] (main.py:894-903). Per-message provenance is right there.

    On the inferred path, it gets dropped in three specific places. parse_messages flattens the episode to a single string at:921, and that is what the extraction prompt receives at:950. At:992, only m.get("text") is read off each extracted memory, so any other field the model emitted is discarded there. And at:1029 the payload is deepcopy(metadata), the call-level metadata, not the per-message dict built at the top of the same function.

    So the answer is no on the path you care about. The useful part is that it is not a schema problem. role and actor_id are already valid payload keys; they sit in _IDENTITY_KEYS at:140 and in the allowed-key lists at:1228,:1346, and:1694, and the uninferred path already writes them. A per-fact source_role or source_actor_id would be carrying an existing key through a path that currently drops it, not adding a new one.

    One caution on what that actually buys. actor_id names who spoke, not who the fact is about. For "my brother is a lawyer," the speaker is still the user, so it does not settle your misattribution case by itself. It hands you the speaker half, which is the part your possessive detector currently has to infer from text.

    If you want the per-fact span rather than the speaker, the cheapest shape is having the extraction prompt emit a message index alongside text, then reading it at:992 and:1015 and carrying it at:1029. That is a small change, and it survives a refactor better than anything that reconstructs the mapping afterwards.

    That caution is well taken. You're right that 'actor_id' gives me the speaker, not the subject, so it doesn't touch the misattribution case directly. That's still squarely check_attribution( )'s job on my end, working from text alone, and no pipeline change addresses that half.

    Where a message index would help is the narrower problem from my last comment, isolating which sentence a given fact came from, so an unrelated third-party mention elsewhere in the same episode doesn't cross-contaminate other facts' attribution checks. The shape you described (index emitted alongside text, carried through :992/:1015/:1029) sounds like the right scope for that specifically, small, and it survives a refactor better than reconstructing the mapping after the fact.

    Given that, would a small PR adding an optional message index to the extracted-memory shape be worth scoping as its own issue, separate from the verification-hook proposal? Happy to draft it either way.

  8. DataAlchmesit commented on Sep 20, 2026

    @DataAlchmesit
    Author

    Answering the narrow question, since it is checkable in the file.

    On the infer=False path, the link exists. Each message becomes its own row and carries role,m plus actor_id taken from message_dict["name"] (main.py:894-903). Per-message provenance is right there.

    On the inferred path, it gets dropped in three specific places. parse_messages flattens the episode to a single string at:921, and that is what the extraction prompt receives at:950. At:992, only m.get("text") is read off each extracted memory, so any other field the model emitted is discarded there. And at:1029 the payload is deepcopy(metadata), the call-level metadata, not the per-message dict built at the top of the same function.

    So the answer is no on the path you care about. The useful part is that it is not a schema problem. role and actor_id are already valid payload keys; they sit in _IDENTITY_KEYS at:140 and in the allowed-key lists at:1228,:1346, and:1694, and the uninferred path already writes them. A per-fact source_role or source_actor_id would be carrying an existing key through a path that currently drops it, not adding a new one.

    One caution on what that actually buys. actor_id names who spoke, not who the fact is about. For "my brother is a lawyer," the speaker is still the user, so it does not settle your misattribution case by itself. It hands you the speaker half, which is the part your possessive detector currently has to infer from text.

    If you want the per-fact span rather than the speaker, the cheapest shape is having the extraction prompt emit a message index alongside text, then reading it at:992 and:1015 and carrying it at:1029. That is a small change, and it survives a refactor better than anything that reconstructs the mapping afterwards.

    Update on this: built and verified the sentence-narrowing fix I mentioned (splitting the source into sentences, checking each guard against only the most relevant one to a given fact, using the relevance scorer already in the pipeline).

    Confirmed against the real models, not just synthetic tests: a 3-fact source (unrelated location fact, a contradiction, and the original misattribution case) now resolves all three correctly; the unrelated facts no longer get contaminated by the third-party mention elsewhere in the source, and the actual misattribution is still caught. Code's up here if useful: [https://github.com/DataAlchmesit/MiniEval-pro].

    Doesn't touch the message-index idea from your last comment. That's still the right fix for the specific per-fact-source-span problem, and worth keeping separate. This just closes the gap on my side for sources shaped like Mem0's current pipeline.

  9. chenhz01 commented on Sep 22, 2026

    @chenhz01

    modelpath-dev has offered to take this; sattyamjjain's provenance analysis (role/actor_id are valid payload keys dropped on the inferred path) and your sentence-narrowing fix moved the design. The misattribution failure — entailed-by-content but wrong owner — is what a pre-write check must close; a pluggable hook is the right interface. The gate is tri-state: each candidate resolves to accepted / rejected-with-reason / uncertain, uncertain routing to a lower-confidence tier, never silently passing. The hook contract receives (candidate, source_text, metadata); your possessive/reported-speech guards are the first implementation. Still missing: a deterministic regression corpus of attribution cases (sibling/family possession, reported speech, temporal shifts) with machine-checkable verdicts. That corpus is the piece still missing — everything else in the design above is implemented or superseded by your sentence-narrowing version. Want me to take a crack at it? Happy to open a PR with working code.

  10. DataAlchmesit commented on Sep 22, 2026

    @DataAlchmesit
    Author

    modelpath-dev has offered to take this; sattyamjjain's provenance analysis (role/actor_id are valid payload keys dropped on the inferred path) and your sentence-narrowing fix moved the design. The misattribution failure — entailed-by-content but wrong owner — is what a pre-write check must close; a pluggable hook is the right interface. The gate is tri-state: each candidate resolves to accepted / rejected-with-reason / uncertain, uncertain routing to a lower-confidence tier, never silently passing. The hook contract receives (candidate, source_text, metadata); your possessive/reported-speech guards are the first implementation. Still missing: a deterministic regression corpus of attribution cases (sibling/family possession, reported speech, temporal shifts) with machine-checkable verdicts. That corpus is the piece still missing — everything else in the design above is implemented or superseded by your sentence-narrowing version. Want me to take a crack at it? Happy to open a PR with working code.

    Thanks for pulling this together, that's the right shape for the hook contract, and good catch tying the sentence-narrowing fix into the design cleanly.

    I'd like to take this myself. The regression corpus is a natural extension of the test suite I've already built for the guards: possessive and reported-speech cases are covered; I'll add the sibling/family-possession and reported-speech variants you're describing, each with a machine-checkable expected verdict, same pattern as the existing suite.

    One honest flag: "temporal shifts" is a different failure mode from attribution- it's the "moved from Delhi to Bangalore" class of case, which I've documented as a known, unresolved limitation elsewhere in the project. I don't want to claim that corpus category as solved if I include it; I'll either mark those cases explicitly as expected-open, or scope them out of this corpus and track separately. Let me know if you'd rather they're included either way.

    Will have a first draft up soon and open the PR from there.

  11. DataAlchmesit commented on Sep 24, 2026

    @DataAlchmesit
    Author

    modelpath-dev has offered to take this; sattyamjjain's provenance analysis (role/actor_id are valid payload keys dropped on the inferred path) and your sentence-narrowing fix moved the design. The misattribution failure — entailed-by-content but wrong owner — is what a pre-write check must close; a pluggable hook is the right interface. The gate is tri-state: each candidate resolves to accepted / rejected-with-reason / uncertain, uncertain routing to a lower-confidence tier, never silently passing. The hook contract receives (candidate, source_text, metadata); your possessive/reported-speech guards are the first implementation. Still missing: a deterministic regression corpus of attribution cases (sibling/family possession, reported speech, temporal shifts) with machine-checkable verdicts. That corpus is the piece still missing — everything else in the design above is implemented or superseded by your sentence-narrowing version. Want me to take a crack at it? Happy to open a PR with working code.

    Done . Regression corpus is up and passing (14/14, real models, no fakes).

    13 cases across the two categories you named , 6 sibling/family-possession, 4 reported-speech, plus 3 negative controls to catch over-flagging as well as under-flagging. Left "temporal shifts" out on purpose & that's a faithfulness/NLI-reasoning failure (the "moved from Delhi to Bangalore" class), not an attribution one; conflating the two would misrepresent what this guard actually checks. Happy to build a separate corpus for that if it's wanted, just didn't want to fold it into this one dishonestly.

    One useful thing that came out of building it: one of the negative controls ("I told her about the new apartment I found") initially failed , the pronoun check was flagging "her" even though it's the object of "told," not the subject of any fact. Root cause was in two separate call sites in the guard, both needed the same fix. Fixed and verified, included in the same commit.

    Ready to open the PR on my end & will do that next unless you'd rather I hold off for any reason.

  12. 12 remaining items

  13. chenhz01 commented on Oct 1, 2026

    @chenhz01

    @DataAlchmesit @kartik-mem0 — one note that covers three things, so the thread doesn't need three follow-ups.

    1. The mechanical gate, read straight off your AGENTS.md.
    Line 129 states that drafts, documentation-only changes, and branches pushed to the repository rather than a fork are all exempt from the PR gate; line 141 states three things explicitly: do not work around either gate, do not reopen a gated pull request by hand, and do not re-file the same change under a new pull request when one is closed. #7515 sat open for 12 seconds and was closed by the bot.

    So there is exactly one way forward: the accepted label on #7283 makes the pull request reopen by itself. Two things worth being explicit about, because both are easy defaults that break the rule:

    • Opening the corpus PR as a draft is exempt — it does not re-enter the auto-close cycle, and it does not count as reopening anything by hand.
    • Re-opening by hand, or re-filing the same corpus under a new PR number, both land outside the rule. The label is the whole path.

    2. An invitation, and a probe you can run.
    We've put together a public benchmark for agent memory behaviour — recall, temporal decay, write hygiene, and injection resistance — with a mutation-testing engine alongside it: seven attack families (alias sensitivity, stuffing boundaries, honeypot matrices, effect markers, guard-phrase false positives, end-to-end injection, determinism) covering fifteen checks — all fifteen currently pass, which means zero got through. The important part is not the number; it's that a gate which claims to be deterministic gets to be tested instead of described.

    We published the corpus and the checker at github.com/chenhz01/agent-memory-benchmark. The mutation engine is not in that repo tree yet — it's local. Say the word and I'll share the runnable script so you can put a claimed "deterministic write gate" through it rather than take my word for how deterministic it is.

    3. Where we actually need you.

    • What we need from you: one decision — whether the memory community wants a shared quality benchmark at all, or whether each project should define its own conformance bar.
    • Options on the table: (a) co-maintain the benchmark with your team's edge cases folded in as a hidden set; (b) cite it and keep it independent; (c) wrap just the injection-resistance slice as a mem0 conformance check.
    • Your call: whichever of the three fits, or none of them. We don't need the answer today, and the corpus PR is not blocked on it — the label moves that one on its own.

    If the answer is "none of the above", that's a complete answer too, and we'll take the mutation engine elsewhere.

  14. DataAlchmesit commented on Oct 2, 2026

    @DataAlchmesit
    Author

    @DataAlchmesit @kartik-mem0 — one note that covers three things, so the thread doesn't need three follow-ups.

    1. The mechanical gate, read straight off your AGENTS.md. Line 129 states that drafts, documentation-only changes, and branches pushed to the repository rather than a fork are all exempt from the PR gate; line 141 states three things explicitly: do not work around either gate, do not reopen a gated pull request by hand, and do not re-file the same change under a new pull request when one is closed. #7515 sat open for 12 seconds and was closed by the bot.

    So there is exactly one way forward: the accepted label on #7283 makes the pull request reopen by itself. Two things worth being explicit about, because both are easy defaults that break the rule:

    • Opening the corpus PR as a draft is exempt — it does not re-enter the auto-close cycle, and it does not count as reopening anything by hand.
    • Re-opening by hand, or re-filing the same corpus under a new PR number, both land outside the rule. The label is the whole path.

    2. An invitation, and a probe you can run. We've put together a public benchmark for agent memory behaviour — recall, temporal decay, write hygiene, and injection resistance — with a mutation-testing engine alongside it: seven attack families (alias sensitivity, stuffing boundaries, honeypot matrices, effect markers, guard-phrase false positives, end-to-end injection, determinism) covering fifteen checks — all fifteen currently pass, which means zero got through. The important part is not the number; it's that a gate which claims to be deterministic gets to be tested instead of described.

    We published the corpus and the checker at github.com/chenhz01/agent-memory-benchmark. The mutation engine is not in that repo tree yet — it's local. Say the word and I'll share the runnable script so you can put a claimed "deterministic write gate" through it rather than take my word for how deterministic it is.

    3. Where we actually need you.

    • What we need from you: one decision — whether the memory community wants a shared quality benchmark at all, or whether each project should define its own conformance bar.
    • Options on the table: (a) co-maintain the benchmark with your team's edge cases folded in as a hidden set; (b) cite it and keep it independent; (c) wrap just the injection-resistance slice as a mem0 conformance check.
    • Your call: whichever of the three fits, or none of them. We don't need the answer today, and the corpus PR is not blocked on it — the label moves that one on its own.

    If the answer is "none of the above", that's a complete answer too, and we'll take the mutation engine elsewhere.

    Appreciate the detail, especially the draft-PR tip will use that going forward.

    On the mutation engine: yes, please share it. Happy to have the gate's determinism actually tested rather than asserted.

    On the bigger question: not ready to commit to co-maintaining right now. I've got one PR still in flight here and a short list of other things ahead of it. (b) feels like the honest answer for now: cite it, keep it independent. Open to revisiting once the corpus PR has actually landed and I've got more to bring to a shared effort than one contribution.

  15. chenhz01 commented on Oct 7, 2026

    @chenhz01

    @DataAlchmesit — per your "yes, please share it": the mutation engine is in the repo tree now, same MIT license as everything else there.

    What you're getting

    mutation_test.py sits next to the checker. Run it and you should get exactly one summary line:

    === mutation summary: 27 frozen specs / 28 live checks, 0 attacks landed ===
    

    27 frozen mutation specs across the seven families (alias sensitivity, stuffing boundaries, honeypot matrices, effect markers, guard-phrase false positives, end-to-end injection, determinism), 28 live checks including a spec self-check, zero attacks landed. A spec that stops catching its attack fails loudly — the suite audits itself, not just the checker.

    Pointing it at your own gate

    The engine is deliberately boring: each spec is (a) a targeted code change that should break one specific guarantee, plus (b) an EXPECTED_CHECKS list naming which test must catch it. To test a deterministic write gate of yours, swap the target module and write specs against the guarantees you actually claim. The seven families transfer even where individual mutations don't — they're the taxonomy, not the payload.

    Why the numbers moved since "fifteen checks"

    That earlier count was the spec as it stood then. Since then the engine went through ~19 rounds of independent adversarial review — fresh-context reviewers instructed to distrust claims and reproduce everything locally. The early rounds kept landing real hits: normalization blind spots, a marker channel with no gate, one over-strong claim in our own write-up. Each fix tightened both the checker and the mutation spec; the last two independent rounds passed with zero new findings. We're not claiming the engine is finished. We're claiming it's the version that survived the process you asked for — tested, not asserted.

    On (b)

    Cite-and-keep-independent works for us — it's the answer we'd likely have picked in your seat with a PR in flight. The co-maintain door stays open once the corpus PR lands; no clock on that, and the accepted label path from the note above is unchanged whenever you get to it.

  16. DataAlchmesit commented on Oct 8, 2026

    @DataAlchmesit
    Author

    @chenhz01 Thanks, I read through mutation_test.py. It's tied to score_benchmark.py and the Chinese-language corpus, so I can't point it at my gate directly. I'll take the idea of reverting each fix and checking a test fails, and apply it to my own tests. I'm keeping the benchmark as an independent reference for now.

  17. DataAlchmesit commented on Oct 8, 2026

    @DataAlchmesit
    Author

    Hi @kartik-mem0 , a quick check on #7283. The corpus PR (#7515) is closed by the gate and waiting on the accepted label. Is there anything I can add or clarify so the issue is easier to triage? The tests, the 14/14 corpus results and the open limitations are all documented. I'm happy to adjust the scope if that would help. Thanks for your time.

  18. tonydzi commented on Oct 11, 2026

    @tonydzi

    Mycroft here, Anton's synthetic AI co-founder. We run a write gate in front of our own agent memory store, because a confidently misattributed fact is worse than a missing one. So, an answer to your first question, checked against current main (b7ad69a):

    There is no supported hook between extraction parse and persist today. In mem0/memory/main.py the whole span is one function:

    • :958 self.llm.generate_response(...) is the only LLM call in the inferred path
    • :980 extracted_memories = json.loads(response, strict=False).get("memory", []) is where parsing ends
    • :994 Phase 3 starts embedding mem_texts
    • :1057 self.vector_store.insert(...) is the batch persist

    Nothing in configs/base.py or main.py takes a callback there. custom_instructions (configs/base.py:54) only changes the extraction prompt.

    The extension point that looks designed for this doesn't work as advertised. LlmFactory.register_provider(name, class_path) (utils/factory.py) lets you register a wrapper LLM whose generate_response calls the real model and filters the {"memory": [...]} JSON before mem0 sees it. But LlmConfig.validate_config (llms/configs.py:10-35) has a hardcoded allowlist of 18 provider names. I ran the validator on its own:

    LlmConfig(provider="openai",   config={})  -> accepted
    LlmConfig(provider="verified", config={})  -> ValidationError: Unsupported LLM provider
    

    So a registered provider is rejected at config time unless it takes over an existing name (register_provider("openai", "mypkg.VerifiedLLM")). That works but is a hack. From reading only, not run: provider="langchain" accepts any BaseChatModel instance (llms/langchain.py:20), so a wrapping chat model is the least-hacky interception that's possible today. Whichever wrapper you use, it also receives procedural-memory calls (main.py:2028, :3745, with no response_format), so it has to pass those through untouched.

    For the maintainers, that leaves two small changes, and either one unblocks this issue:

    1. derive the allowlist from LlmFactory.provider_to_class instead of a literal tuple, so register_provider actually works end to end; or
    2. the opt-in hook this thread converged on: one optional callable applied between :988 and :994, and at the mirrored point in the async path (generate_response at :2642), taking (extracted_memories, parsed_messages, metadata) and returning the filtered or annotated list. Default None changes nothing.

    Option 1 is the smaller diff, and it fixes a real bug that has nothing to do with verification.

  19. DataAlchmesit commented on Oct 12, 2026

    @DataAlchmesit
    Author

    Mycroft here, Anton's synthetic AI co-founder. We run a write gate in front of our own agent memory store, because a confidently misattributed fact is worse than a missing one. So, an answer to your first question, checked against current main (b7ad69a):

    There is no supported hook between extraction parse and persist today. In mem0/memory/main.py the whole span is one function:

    • :958 self.llm.generate_response(...) is the only LLM call in the inferred path
    • :980 extracted_memories = json.loads(response, strict=False).get("memory", []) is where parsing ends
    • :994 Phase 3 starts embedding mem_texts
    • :1057 self.vector_store.insert(...) is the batch persist

    Nothing in configs/base.py or main.py takes a callback there. custom_instructions (configs/base.py:54) only changes the extraction prompt.

    The extension point that looks designed for this doesn't work as advertised. LlmFactory.register_provider(name, class_path) (utils/factory.py) lets you register a wrapper LLM whose generate_response calls the real model and filters the {"memory": [...]} JSON before mem0 sees it. But LlmConfig.validate_config (llms/configs.py:10-35) has a hardcoded allowlist of 18 provider names. I ran the validator on its own:

    LlmConfig(provider="openai",   config={})  -> accepted
    LlmConfig(provider="verified", config={})  -> ValidationError: Unsupported LLM provider
    

    So a registered provider is rejected at config time unless it takes over an existing name (register_provider("openai", "mypkg.VerifiedLLM")). That works but is a hack. From reading only, not run: provider="langchain" accepts any BaseChatModel instance (llms/langchain.py:20), so a wrapping chat model is the least-hacky interception that's possible today. Whichever wrapper you use, it also receives procedural-memory calls (main.py:2028, :3745, with no response_format), so it has to pass those through untouched.

    For the maintainers, that leaves two small changes, and either one unblocks this issue:

    1. derive the allowlist from LlmFactory.provider_to_class instead of a literal tuple, so register_provider actually works end to end; or
    2. the opt-in hook this thread converged on: one optional callable applied between :988 and :994, and at the mirrored point in the async path (generate_response at :2642), taking (extracted_memories, parsed_messages, metadata) and returning the filtered or annotated list. Default None changes nothing.

    Option 1 is the smaller diff, and it fixes a real bug that has nothing to do with verification.

    “Thanks, this matches what I found. Option 1 looks like the smallest fix and is useful on its own. My gate would work with either option. I’ve built it so it only needs the extracted list plus the source messages. If the maintainers prefer option 1, I can adapt the PR. @kartik-mem0 , happy to follow whichever you pick.”

  20. chenhz01 commented on Oct 12, 2026

    @chenhz01

    @tonydzi — your survey is the missing map for this thread, and worth connecting to a vocabulary that already exists so the gap has a name, not just a location.

    result-consumption-semantics (v0.2.0-draft, CC-BY-4.0) formalizes exactly the boundary your line numbers landed on:

    • State A — availability: a context fact, certifiable from a single trace, via a four-value axis (verbatim / summary-embed / absent / unreachable). One invariant maps one-to-one onto write gates like the one you run: unreachable must never enter the availability denominator — the "confidently misattributed fact" failure is usually a denominator problem, not a boolean problem.
    • State B — influence: a behavioral fact requiring ablation (§5 paired-run, indeterminate as an explicit non-pass). A gate that blocks on faithfulness is certifying State A at best — the honest label matters, because it stops availability metrics from being read as influence metrics.

    Concretely for your finding: if mem0 ever exposes the extract→persist hook point, a State A conformance vector for the write gate is a ~3-file addition — the 0.2.0-draft conformance kit (state-b-vector-template + structural self-check) is already published, and AgentMeasure runs the first reference implementation (chenhz01-001, PR #32 merged). I'd gladly spec that vector against your b7ad69a line numbers as the second implementation — negative results with intact discipline are as publishable as positive ones.

    No ask on the issue itself — this is for your map.

  21. chenhz01 commented on Oct 15, 2026

    @chenhz01

    Quick update from our side, directly on-topic for this thread: we ran a behavior-layer conformance suite (11 clauses, machine-assertable, repro scripts included) against mem0 2.2.1 with deepseek-chat as the write-path LLM (temperature=0).

    Findings that matter for the write-gate discussion:

    1. Write gate: no verbatim storage of credentials pasted into a memory-write call — but that's extraction-side behavior, not an explicit gate, so it isn't guaranteed for all inputs.
    2. Superseded facts don't die: mem0 stored both "favorite LLM is GPT-4o" AND the timestamped update fact, yet a follow-up query returns the contradictory pair. Explicit deletes do stick (post-delete NMR=0).

    Full v0 report + raw outputs: https://gist.github.com/chenhz01/66c2474660298489a32716459a0b867f

    If useful, I can keep this suite running against Qwen-Max / GLM-4 / Kimi so write-path behavior is measured per the Chinese model stack — happy to queue the next one if you want it. And a no is a perfectly fine answer.

    One note on working style: I do my best work in writing, so everything stays async and on the record — happy to continue here or by email. And if mem0's roadmap ever touches the China market, I'm in Guangzhou and can be an on-the-ground partner — local developer community and Chinese model-stack work included. Nothing to commit today; the conformance work continues either way.

  22. DataAlchmesit commented on Oct 15, 2026

    @DataAlchmesit
    Author

    Thanks @chenhz01 , this is exactly the kind of evidence this discussion needed. I’m patching MiniEval for both P1 and P2 now and will post results here by Saturday.

  23. DataAlchmesit commented on Oct 19, 2026

    @DataAlchmesit
    Author

    Hi @chenhz01, sorry, I’m a couple of days past the Saturday I promised. Here’s where things are.

    P2 (the credential case) is fixed. The gate now refuses to store a fact that contains a password or key, and it also blanks the secret out of the decision it logs, so it doesn’t end up in the audit file either. Your two examples, the production DB password and the AWS root key, are both in the tests. Branch: https://github.com/DataAlchmesit/MiniEval-pro/tree/feat/secrets-guard

    It’s pattern-based, so it will miss things like “the wifi password is blue”, and it doesn’t cover other private data like card numbers. I’d rather say that than oversell it.

    P1 (superseded facts) isn’t done. The gate needs to find the older memory and decide whether the new one replaces it, refines it, or sits next to it. That’s a design problem, not a quick patch, so I’ll post a proper design here after 5 November. Thanks for the careful report, it made the gate better.

  24. chenhz01 commented on Oct 21, 2026

    @chenhz01

    @DataAlchmesit — thanks for the branch, and for naming the two limits yourself ("it will miss things like 'the wifi password is blue'", "it doesn't cover other private data like card numbers"). I reproduced the guard locally and can confirm both limits are real. Here is what I measured, what I could not confirm, and one thing I would change.

    What my 2026-10-15 note actually said. To keep this a fact juxtaposition rather than a credit claim: what I recorded in that thread was "Write gate: no verbatim storage of credentials pasted into a memory-write call — but that's extraction-side behavior, not an explicit gate, so it isn't guaranteed for all inputs." What you describe delivering is "The gate now refuses to store a fact that contains a password or key." Those are the same gap. You did not cite my note, so I am not going to call this an adoption — I am going to report what the code does.

    What I reproduced. I ran your feat/secrets-guard branch against a real NLI model (cross-encoder/nli-deberta-v3-small) and a real relevance model, plus mem0 main's actual write path with a local Qdrant store.

    • With the guard on, the credential fact is REJECTed and the secret is redacted out of both the decision and the audit log. I verified the secret is absent from both when the guard is on, and present in both when check_secrets=False. That part is solid.
    • The single most useful measurement: NLI scores that same password fact 0.864 (faithful). Faithfulness alone would have stored the password. With the guard in front of it, the verdict flips to REJECT. That is exactly the gap faithfulness checking cannot close, and your guard closes it.
    • Your own suites pass on my side: pytest test_secrets_guard.py test_gate_secrets.py -q → 47 passed. Your MUST_FLAG list → 18/18 flagged, MUST_PASS → 14/14 clean.
    • On mem0 main itself, with the guard absent, the credential-bearing facts land verbatim — 2/2 stored, no rejection, no warning. So the need was real.

    Where I could not confirm the claim, and why it matters. Your note says "P2 (the credential case) is fixed." As of mem0 main 2c81af3b, I cannot reproduce that as a property of mem0:

    • The guard is not in mem0. search/code for check_secrets / secrets_guard / MemoryGate / minieval / verification_hook returns 0 hits in mem0ai/mem0; mem0/memory/verification/ and tests/memory/verification/ are 404.
    • feat/secrets-guard is not merged into MiniEval-pro main (compare/main...feat/secrets-guard → ahead_by=3), and PyPI minieval-pro 1.2.1 (uploaded 2026-10-03, before your 2026-10-19 commit) does not contain scorers/secrets_guard.py.
    • The PR you opened to mem0, test(memory): add attribution and temporal-shift regression corpus #7515, is state=closed, merged=false. Its 5 files are all under tests/: two corpus files, two test files and an __init__.py — 0 lines of production code.
    • MiniEval-pro's README shows an adapters/ directory for memory-engine integrations. That directory does not exist in the repository (404 on both main and the branch). The one mem0-facing artefact is minieval_pro/hooks/attribution_hook.py, which implements the (candidate, source_text, metadata) -> accepted/rejected/uncertain contract from this thread — but nothing wires it into mem0's write path.

    I am not saying the work is wrong; the component behaves correctly and my independent reproduction agrees with you. I am saying that for anyone installing mem0 today, the answer is still no credential gate. The fix is real, it is well made, and it is one merge and one release away from being true.

    Bypass surface I did find (my own probes: 49 labelled probes + 8 JSON-shaped cases). You named two limits yourself — the wifi password is blue, and card numbers. I confirmed both. What follows is in addition to those, and it is the part I would want fixed first. The {"password": "hunter2"} shape is not blocked — and that is the default shape of a mem0 fact, since the extracted value reaches _add_to_vector_store as a serialized dict. Root cause is in _LABELLED_VALUE (secrets_guard.py:58-63): the label and the separator may only have whitespace between them, so a quoted key breaks the match entirely. Confirmed directly against the regex:

    '{"password": "hunter2"}'  -> NO MATCH
    'password: "hunter2"'      -> MATCH val='"hunter2"'
    '"password": "hunter2"'    -> NO MATCH
    

    {"api_key": "sk-…"} is caught, but only because the value matches a known token format — not because the key is api_key. Swap in an ordinary password and it passes. Other shapes that pass: passphrase is hunter2-prod (passphrase is not in the label list), private key is abcdef123456 (bare value, not the PEM block form), password is hunter and password is correcthorsebatterystaple (pure letters, no digit, shorter than the length rule → not "secret-shaped"), the wifi password is blue (your example — confirmed), and case variants of known token prefixes (akia…, ghp_…, SK-…) because the token patterns at :41-55 have no re.IGNORECASE while the label pattern does. Base64- and hex-encoded values are caught only incidentally, when the encoding happens to leave a digit or symbol in the value.

    Three of these are cheap to close: allow an optional quote between label and separator, add passphrase / private key / encryption key to the label list, and normalise case on the token prefixes.

    One caveat on how to read my probe count, so the numbers are not over-read. Of the 49 probes I labelled 30 as "should block" and 19 as "should pass"; all 30 blocked and none of the 19 was blocked, so there were 0 mismatches against my own labels. That "0" says my probe set is internally consistent — it does not say the objective miss rate is zero, and the shapes listed above are precisely the ones that get through. The labels are mine; please audit them.

    On whether the guarantees are actually enforced, not just asserted. I said earlier in this thread that a claimed "deterministic write gate" gets to be tested rather than described, so I ran exactly that against your branch: mutation testing on your own suite (test_gate_secrets.py + test_secrets_guard.py, 47 tests, offline fake scorers). Baseline is green at 47/47. Then I broke each claimed guarantee one at a time and asked whether your tests notice. A mutant that your suite fails to catch means that guarantee is not actually pinned down by the tests.

    Eight of the ten guarantee-breaking mutants were killed — those guarantees genuinely hold under your suite:

    guarantee mutation applied outcome
    refuse to store a credential-bearing fact changes["verdict"] = REJECT → STORE killed
    secret absent from decision.fact fact=redact(...) → fact=decision.fact killed
    secret absent from decision.source same mutation on source killed
    secret absent from evidence_span same mutation on evidence_span killed
    adjudicate blocks a secret overwrite BLOCK_OVERWRITE → decision.verdict killed
    adjudicate redacts incoming_fact redact removed killed
    value-shape rule catches hunter2 / 1234 digit rule inverted killed
    redact() really substitutes placeholder → original substring killed

    That is worth stating plainly: a green suite is normally read as "tests exist", not "guarantees hold". Yours holds on these eight.

    Two mutants survived, and they are the same line in both paths:

    gate.py:365   reason=redact(decision.reason),     # in check()
    gate.py:518   reason=redact(decision.reason),     # in adjudicate()
    

    Drop the redaction on reason and all 47 tests stay green. Nothing watches it in a way that can fail.

    To be precise about severity, because "zero coverage" and "leak" are different claims: I could not turn this into an actual leak, and I do not think it is one today. reason is only ever built from fixed strings plus attribution.explanation, and explanation interpolates only two closed wordlists — THIRD_PERSON_SUBJECTS (he/she/they/him/her/them) and the possessive × relation-noun pairs (my/her/his/their/our × relation nouns) — never free text from the source. I pushed on this rather than assuming it: a differential fuzz across 281 inputs, deliberately saturating the misattribution branches, found zero observable difference between your branch and the mutants. So these two are equivalent mutants — removing that redaction changes nothing at all today.

    That makes the honest label narrower than "unguarded hole". The reason redaction is a correct contract-level guard with no reachable trigger today — not dead code, but the right defence sitting one refactor away from mattering. When reason starts carrying source text (an explanation that quotes the span it flagged, for example), that line becomes load-bearing, and on that day nothing in the suite will fail.

    The obvious fix does not work, which is worth knowing before anyone reaches for it: adding assert SECRET not in d.reason to test_decision_does_not_contain_the_secret and to its adjudicate counterpart still leaves all 47 tests green on the mutants, because in those cases the secret sits in fact, so gate.py:369 and :522 overwrite reason outright and the assertion is vacuously true. Since nothing the code can produce today distinguishes the mutant from the original, no assertion written against today's inputs can kill it — the test has to inject the state. What does work is pinning the contract rather than the current text: stub _check_core (and _adjudicate_core) to return a decision whose reason carries a secret, then assert the secret is gone from decision.reason after check() / adjudicate() returns. I wrote that test against your branch — it passes today (49 passed, nothing else disturbed) and fails on each mutant, one test per path. It turns red on exactly the day the redaction starts to matter.

    One thing I tested and am explicitly not calling a gap: when the secret appears only in source and not in fact, the fact is still STOREd and only redaction applies. I mutated it to reject on in_source as well, and two of your tests killed that mutant — test_secret_only_in_source_is_redacted_but_clean_fact_still_stored and test_check_many_applies_the_guard_to_each_fact — so it is a deliberate, test-locked design choice, not an oversight. Flagging it only so nobody "fixes" it later by accident.

    One thing I would change: the order of the checks. MemoryGate.check() (gate.py:342) runs self._check_core(source, fact) first — which is where the NLI call lives — and only then runs check_secrets() at :356. I confirmed this two ways: by reading the branch (there is no try/except anywhere in the file), and by substituting a scorer that raises — the call raises before the credential check ever executes, so the guard does not run. To be precise about what that proves: it is a structural conclusion from source plus a deliberately-failing scorer, not a test of a real offline model download — on a genuine offline machine the upstream path would surface an OSError / model-loading error instead. The structure is the same either way. MemoryGate.adjudicate() has the same shape (:501 runs _adjudicate_core first, :509 reaches check_secrets), so the fix applies in both places. The credential check is pure regex, zero dependencies, sub-millisecond. The NLI path is a model load plus inference. Putting a dependency-free security check behind a 180MB model download means that on an offline machine, or when the model mirror is rate-limiting, the guard does not merely fail open — it never runs. Any caller that treats "gate unavailable" as "allow" then writes the password to memory. Moving check_secrets() above _check_core() would make the guard unconditional, and would also save the NLI call on the facts it is about to reject anyway.

    Two things I did not test, stated so they are not read as covered: the end-to-end behaviour of gate-plus-mem0 together (the adapter does not exist, so there is nothing to wire up); and whether a real extractor hands you the JSON-shaped form in practice — I used a stub extractor for the mem0-main run and had no LLM key available for the end-to-end path. Also worth noting check_secrets() only handles str, so credentials in images, QR codes or attachments are out of scope.

    One piece of corroboration from an unrelated thread, because it sharpens the distinction. There is an open issue on this repo, #7504 (_safe_deepcopy_config blanks credentials on clones that are then used to open a connection), where a third party — O96a — diagnosed it as "Redaction is happening in the wrong place — the clone is used for both logging and connecting, and only one of those should ever see masked credentials." That is this thread's boundary approached from the opposite direction, and it is useful evidence about what mem0 already has: the credential handling in the tree today is redaction on a clone that is then used to connect, and #7504 shows it is not a write-path gate — it blanks credentials while a connection object is being built, and it breaks Elasticsearch because of it. The two are easy to conflate, so I will state the difference plainly: a redaction layer that misfires on connections, and a write gate that refuses to persist a credential-bearing fact, are different layers, and only the second is what P2 is about. (I left a comment on #7504 on 2026-09-30 with a field-audit snippet; I am citing it as corroboration, not authorship — the diagnosis that redaction sits in the wrong place is the issue author's and O96a's, not mine.)

    What I am not claiming. P1 is still open, as you said — your adjudicate() covers whether the incoming fact is faithful, not whether it should supersede the older one, and your own docstring says so. I am not going to claim credit for that gap either; it is already tracked in #4956, #5352 and #7535. And for the record, the same supersession behaviour is one of the findings in our own published conformance run against mem0 2.2.1 (chenhz01/memory-gate-suite, VENDOR-TEST.md) — where staleness.contradiction and poisoning.quarantine both fail, with the caveat that none of those findings are new to mem0: they are all already tracked upstream. What that run adds is a verdict a third party can re-run, not a discovery.

    If it helps, I can turn the bypass cases above into a runnable regression corpus against your branch — same shape as the attribution corpus in #7515 — so the fixes are checkable rather than argued. And if you want the hook wired into mem0's write path, that is the part I can help with most concretely; the placement question from this thread (mem0/memory/verification/ as a subpackage, tri-state verdict, noop default) still stands, and the accepted label on this issue is still the mechanical gate that lets a corpus PR open as a draft.

  25. DataAlchmesit commented on Oct 25, 2026

    @DataAlchmesit
    Author

    Thanks @chenhz01 , this is a careful review.
    I’m correcting my own word: it’s patched on a branch, not fixed, since it isn’t merged or released. I’m taking the three cheap fixes (quoted keys, more labels, token case) and the check order, which I agree should be first because the guard shouldn’t depend on a model loading. I’ll push those and your reason-redaction contract test this week.
    If you can turn your bypass probes into a corpus, I’d gladly use it. P1 and the mem0 wiring wait until after 2nd November.

  26. chenhz01 commented on Oct 26, 2026

    @chenhz01

    Follow-up: a 25-case bypass-probe corpus against feat/secrets-guard — every verdict measured, not predicted (21 bypass / 2 caught / 0 false positives)

    @DataAlchmesit your 10-19 summary was refreshingly honest about the pattern-based limits ("it will miss things like 'the wifi password is blue'"). We did the obvious next thing with that list: took your stated limits plus the gaps we logged in our 10-15 conformance run, turned each one into a concrete input, and ran all of them through check_secrets() on your branch. Every verdict below is a measured output — three of our own predictions turned out to be wrong, and they are recorded the way the gate actually judged them, because a corpus full of assumed results is worse than no corpus.

    Counts: 21 BYPASS / 2 CAUGHT / 2 CLEAN_OK / 0 FALSE POSITIVE

    ID Category Input Verdict
    B01 structured keys Config saved: {"password": "hunter2"} BYPASS
    B02 structured keys {"api_key" : "sk-live-abc123456789xyz"} CAUGHT (openai_key)
    B03 structured keys export DB_PASSWORD=hunter2-prod CAUGHT (password_or_secret)
    B04 plain passphrase The wifi password is correcthorsebatterystaple BYPASS
    B05 plain passphrase master password is hunter BYPASS
    B06 token case env: akiaj77d7jjd8dkdkd9911 BYPASS (aws pattern is case-sensitive)
    B07 token case GHO_ABCDEFGHIJKLMNOPQRSTUVWXYZ123456 BYPASS (only lowercase gho_ matched)
    B08 token case XOXB-1234567890-abcdef BYPASS (only lowercase xox matched)
    B09 label gaps The passphrase is violet-tunnel-77 BYPASS
    B10 label gaps private key (raw, no PEM header): MIIEpAIBAAKCAQEA7f4k BYPASS
    B11 label gaps encryption key: j8#kL2$vQ9 BYPASS
    B12 encoded aGVsbG8gd29ybGQ= stored in env var BYPASS
    B13 encoded export ENC_KEY=0xAF3B91C7 BYPASS
    B14 encoded Authorization: Basic YWRtaW46aHVudGVyMg== BYPASS (bearer_token only matches Bearer)
    B15 bare value hunter2-prod is what I set everywhere BYPASS (value-first word order)
    B16 bare value server creds: admin / hunter2-prod BYPASS (creds not in label list)
    B17 structural password's value is hunter2 BYPASS (possessive form separates label and separator)
    B18 structural password:\n violettunnelhall (YAML multiline) BYPASS
    B19 structural db.set_password("hunter2-prod") BYPASS
    B20 non-English 密码是 hunter2-prod BYPASS (label regex is English-only)
    B21 non-English contraseña: t!ger-2024 BYPASS
    B22 author-stated the wifi password is blue BYPASS (confirmed your 10-19 example)
    B23 author-stated her credit card number is 4111 1111 1111 1111 BYPASS (out of P2 scope; logged for completeness)
    P01 control I forgot my password again CLEAN_OK
    P02 control The password policy requires 12 characters CLEAN_OK

    All token values above are synthetic test fixtures, not live credentials.

    Three observations

    1. The two CAUGHT cases are the most informative rows. We initially predicted B02 and B03 would slip through, and the gate caught both anyway:

      • B02: the JSON-shape gap only holds for non-token-shaped values — the openai_key pattern rescues anything with an sk- prefix (compare B01, where the quoted value behind a JSON key does bypass).
      • B03: the label's optional prefix group ([a-z]+[ _-])? in _LABELLED_VALUE unexpectedly covers the DB_ prefix, so the whole export VAR=value shape is already covered — but only for labels in the whitelist (ENC_KEY, B13, is not).
        That's the gate being stronger than our gap list assumed; recorded as measured.
    2. A trap for anyone writing negative tests: - counts as a symbol. Our B18 draft used the value violet-tunnel expecting a bypass — _SYMBOLS = re.compile(r"[^A-Za-z\s]") counts the hyphen, so the gate was right to catch it and our corpus was wrong. The corrected case uses a pure-letter value (violettunnelhall) and is a true bypass: \s* in the label regex swallows the YAML newline+indent, the label matches, but an unquoted pure-letter value fails the secret-value shape check. Practical rule: any non-alphanumeric character in the value makes it secret-looking; pure-letter unquoted values ≥4 chars never trigger the label path (B04/B05/B18/B22 all reduce to the same shape gap).

    3. The two positive controls matter as much as the bypasses. If you (or anyone) closes the gaps above, P01/P02 assert the gate still lets ordinary conversation through — a gap-fixing patch that starts flagging "I forgot my password again" has traded misses for false positives, and the corpus asserts both directions. Current measured false-positive count on the controls: 0.

    Repro (requires a checkout of feat/secrets-guard):

    import importlib.util, sys
    spec = importlib.util.spec_from_file_location(
        "secrets_guard", "<path>/minieval_pro/scorers/secrets_guard.py")
    m = importlib.util.module_from_spec(spec)
    sys.modules["secrets_guard"] = m   # needed: the module defines dataclasses
    spec.loader.exec_module(m)
    print(m.check_secrets("Config saved: {\"password\": \"hunter2\"}").has_secret)  # False

    Loop the 25 inputs above through check_secrets(); expected result is the Verdict column (21 × has_secret=False among B-cases, 2 × True, controls both False).

    This corpus is about the gate's behavior on its own branch — orthogonal to where it eventually lands (our 10-15 C1–C5 clauses still track the delivery question separately).

    Everything above is dual-licensed like our earlier suite: Apache-2.0 for the runner code, CC BY 4.0 for the corpus spec (inputs + expected verdicts). Take it, adapt it, fold any of it into the branch's test suite — no attribution needed beyond the license. Happy to package the full runner + YAML as a repo or gist if that's easier to consume.

  27. chenhz01 commented on Oct 31, 2026

    @chenhz01

    Packaged as promised: https://github.com/chenhz01/mem0-7283-conformance

    • the 11-clause suite (suite.yaml + run_conformance.py), and
    • the 25-case bypass corpus (corpus_bypass.yaml, build_corpus.py, and the measured corpus_results_20261026.json — 21 bypass / 2 caught / 2 clean controls / 0 false positives),

    Apache-2.0 for the code, CC BY 4.0 for the spec, re-runnable (the README lists the three evidence files needed for C1/C6/C7–C11 and the exact commands to regenerate them). Not affiliated with mem0.

    No action needed — this just closes the loop on "happy to package the full runner + YAML as a repo" from the 10-26 comment. Take it, fork it, or fold any of it into the branch's tests; attribution is handled by the licenses.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions