Skip to content

fix(engine): drop null metadata values at retain and recall (#3209) - #3531

Open
NovaLux12 wants to merge 1 commit into
vectorize-io:mainfrom
NovaLux12:fix/retain-drop-null-metadata
Open

fix(engine): drop null metadata values at retain and recall (#3209)#3531
NovaLux12 wants to merge 1 commit into
vectorize-io:mainfrom
NovaLux12:fix/retain-drop-null-metadata

Conversation

@NovaLux12

Copy link
Copy Markdown

Closes #3209

Problem

MemoryFact.metadata is typed dict[str, str] | None, but retain accepts
arbitrary user-supplied JSON metadata. A null value (e.g.
{"ocr_engine": null}) was stored verbatim, and every read path that returns
MemoryFact — recall, consolidation, mental-model refresh — then failed
Pydantic validation (Input should be a valid string), permanently wedging
the affected bank.

Change

Normalize on both ends, per the plan in the issue:

  • Write pathRetainContent.__post_init__ drops null-valued metadata
    keys at construction, so facts extracted from it (metadata=content.metadata
    in fact_extraction) are stored canonically. Single chokepoint: covers the
    main retain path, delta retain, recovery, and transfer import. Document
    metadata in retain_params is untouched (built from the raw request dicts).
  • Read pathMemoryFact.parse_metadata drops null-valued keys instead
    of coercing them to the string "None", so already-stored legacy rows
    become readable again with no data migration. Existing string coercion for
    other non-string values is preserved.

Verification

  • New tests:
    • tests/test_response_models.py (read path): null metadata value omitted,
      null inside a JSONB-string is omitted, all-null becomes {}.
    • tests/test_retain_metadata_normalization.py (write path): RetainContent
      drops nulls, keeps non-string values as-is, and _build_contents (the API
      ingestion funnel) normalizes a null-metadata request.
  • uv run pytest tests/test_response_models.py tests/test_retain_metadata_normalization.py tests/test_retain_orchestrator_mapping.py -q27 passed.
  • Consumer slice (test_large_document_replacement.py,
    test_retain_entity_prune_race.py, test_validation_result_enrichment.py,
    test_response_exclude_none.py): 16 passed; the 9 errors are all the
    same pre-existing environmental ImportError: pg0-embedded is required for embedded PostgreSQL (optional embedded-db extra not installed in this
    venv), unrelated to this change.
  • ruff check, ruff format --check, and ty check pass on all changed files.

Retain accepts arbitrary JSON metadata; a null value (e.g. {"ocr_engine":
null}) stored verbatim made every read path that returns MemoryFact fail
dict[str, str] validation — recall, consolidation, and mental-model refresh
errored for the affected bank. Normalize on both ends: RetainContent drops
null-valued keys at construction (canonical storage) and
MemoryFact.parse_metadata drops them for legacy rows, preserving the
existing string coercion for other non-string values.

Closes vectorize-io#3209
@chaoyuan-guo

chaoyuan-guo commented Aug 17, 2026

Copy link
Copy Markdown

One write-side gap remains: delta updates bypass RetainContent normalization.
Both _delta_metadata_only and the partial-delta survivor sync pass raw retain_params.metadata to update_memory_units_metadata_and_tags. As a result, new or changed facts omit null-valued keys, while existing SQL-backed memory_units can receive them during re-retain. MemoryFact filters them on read, so recall hides the inconsistency.
Could these two paths use normalized RetainContent.metadata, while documents.retain_params.metadata continues to preserve the raw input?
The current _build_contents test only checks construction. Regression coverage should exercise:

  • unchanged content, reaching _delta_metadata_only;
  • a partial delta with unchanged survivors.

In both cases, documents.retain_params.metadata should preserve the null value, while memory_units.metadata should omit the null-valued key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recall: MemoryFact rejects stored metadata containing null values (retain accepts, recall fails)

2 participants