fix(memorize): silent memory item drops in XML parser and LLM-mode retrieval#421
fix(memorize): silent memory item drops in XML parser and LLM-mode retrieval#421Meur3ault 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.
|
Thank you for the contribution and the thoughtful write-up. |
📝 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).