fix(memorize): silent memory item drops in XML parser and LLM-mode retrieval#421
Open
Meur3ault wants to merge 3 commits into
Open
fix(memorize): silent memory item drops in XML parser and LLM-mode retrieval#421Meur3ault wants to merge 3 commits into
Meur3ault wants to merge 3 commits into
Conversation
The _parse_memory_element check required both <content> and <categories> to be non-empty, but the memory-type prompts (profile, event, knowledge, behavior) explicitly state that the categories field may be empty. Memory items that did not match any pre-configured category were silently dropped during memorize(). Now only <content> is required; an empty or missing <categories> defaults to [].
The _find_xml_boundaries whitelist matched only "behaviors"/"events"/ "skills" (plural-only) and was missing "tool" entirely. Custom prompts using singular semantic root tags (e.g. <event>, <skill>) or the <tool> root caused the entire LLM response to be discarded. The whitelist now covers both singular and plural forms of every MemoryType value.
…M-mode retrieval Memory items whose LLM-extracted categories match none of the configured ones were left without any CategoryItem relation. LLM-mode retrieval joins items through that relation table (_format_items_for_llm), so those items were unreachable from the LLM path even though they sat in the database with a usable embedding. An 'uncategorized' category is now auto-created at category init (memorize_config.enable_uncategorized_fallback, default True). Items with no matching configured category are linked to it. Summary updates skip the fallback category since aggregating unrelated facts produces noise and wastes tokens; its static description embedding is enough for route_category to score it when nothing else matches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 Pull Request Summary
Two parser bugs in
src/memu/app/memorize.pythat silently drop memory items duringmemorize(), plus a follow-up so the recovered items stay reachable from LLM-mode retrieval.✅ What does this PR do?
Three atomic commits in
src/memu/app/memorize.py:1. Preserve memory items with empty
<categories>(cbfac4b)_parse_memory_elementrequired both<content>and<categories>to be truthy:But the prompts at
prompts/memory_type/{profile,event,knowledge,behavior}.pyall state"categories": [...can be empty]. Memories that don't fit a pre-configured category were dropped with no log line. Now only<content>is required; empty categories defaults to[].2. Expand the XML root-tag whitelist (
e116f4d)_find_xml_boundariesrecognised only["item", "profile", "behaviors", "events", "knowledge", "skills"].MemoryTypeindatabase/models.pyincludestooland singularbehavior/event/skill. The whitelist is extended to cover both forms of every MemoryType. Latent today because every built-in prompt wraps in<item>, but any custom prompt using semantic root tags returned[]with only aCould not find valid root tagwarning.3. Link uncategorized items to a fallback category (
8284ff1)After fix 1, items with no matched category land in the DB but with no
CategoryItemrelation. LLM-mode retrieval joins items via that table (retrieve.py:_format_items_for_llm), so those items remained unreachable from the LLM path. (RAG-mode was unaffected because itsrecall_itemsdoes a global vector search.)An
uncategorizedcategory is now auto-created at category init (memorize_config.enable_uncategorized_fallback, defaultTrue). Items whose extracted category names match nothing get linked to it. The category is seeded with a static summary at creation (equal to its description) becauseretrieve.py:_rank_categories_by_summaryfilters oncat.summary; the seeding goes throughupdate_category()so it persists on SQLite and Postgres backends whereget_or_create_categoryreturns a detached or copied instance. Dynamic summary updates skip the fallback since aggregating heterogeneous items into one summary is noisy and wastes tokens.🤔 Why is this change needed?
The first two bugs cause silent data loss during the core
memorize()flow — extracted facts simply never reach storage and there is no log to attribute the loss to. The third change closes the gap so that data preserved by fix 1 is also retrievable through the LLM-mode path, not just RAG-mode.Related discussion: the long-term memory drift reported in #381 is made strictly worse by silent drops, so fixing this is upstream of any drift work.
🔍 Type of Change
✅ PR Quality Checklist
fix:×3 commits)description=; no public API docs needed)enable_uncategorized_fallbackflag defaults toTrueand adds oneuncategorizedentry tolist_memory_categories(); setting it toFalserestores the old behaviour exactly📌 Optional
CategoryConfig(name="uncategorized")→ not duplicated, user's config winsnot cat.summaryguard)update_category()flowMemoryCategory.embeddingis set at init but never consumed by retrieval (route_category re-embedscat.summaryinstead)[]from_rank_categories_by_summarywhen no category has been summarised yet — the fallback's seeded summary partially mitigates this23 new unit tests across
tests/test_xml_parser.pyandtests/test_uncategorized_fallback.py, no API key needed. The fallback tests use the in-memory backend with a deterministic stub LLM client, so they exercise the real category creation, item persistence, relation linking, and summary-update code paths end-to-end.make testreports 100 passed, 1 skipped;make checkpasses pre-commit, mypy, and deptry (the pre-existinguv.lock1.5.0 vspyproject.toml1.5.1 mismatch onmainis not touched by this PR).