headroomlabs-ai/headroom · error · ValueError

metadata ({len(metadata)}) must match memory_ids ({len(memor

Error message

metadata ({len(metadata)}) must match memory_ids ({len(memory_ids)}) length

What it means

Second invariant check in FTS5TextIndex.index_batch: when an optional metadata list is supplied, it must have exactly one dict per memory_id (same length), so each indexed row gets its metadata in the single transaction. Raised immediately after the ids/texts length check passes.

Source

Thrown at headroom/memory/adapters/fts5.py:167

        metadata: list[dict] | None = None,
    ) -> None:
        """Index multiple memories in a single transaction.

        Args:
            memory_ids: List of unique identifiers.
            texts: List of text contents to index.
            metadata: Optional list of metadata dicts (one per memory).

        Raises:
            ValueError: If memory_ids and texts have different lengths.
        """
        if len(memory_ids) != len(texts):
            raise ValueError(
                f"memory_ids ({len(memory_ids)}) and texts ({len(texts)}) must have same length"
            )

        if metadata is not None and len(metadata) != len(memory_ids):
            raise ValueError(
                f"metadata ({len(metadata)}) must match memory_ids ({len(memory_ids)}) length"
            )

        metadata = metadata or [{} for _ in memory_ids]

        with self._get_conn() as conn:
            # Delete existing entries
            conn.executemany(
                "DELETE FROM memory_fts WHERE memory_id = ?",
                [(mid,) for mid in memory_ids],
            )

            # Prepare batch data
            batch_data = []
            for memory_id, text, meta in zip(memory_ids, texts, metadata):
                user_id = meta.get("user_id", "")
                session_id = meta.get("session_id", "")
                category = ""  # Deprecated - kept for backwards compatibility

View on GitHub (pinned to 322425c43b)

Solutions

  1. Compare the counts in the message; regenerate metadata with a list comprehension over memory_ids so lengths lock together.
  2. If metadata is unknown for some rows, pad with empty dicts: metadata=[m.get(mid, {}) for mid in memory_ids].
  3. Omit the metadata argument entirely when you don't have per-id data — it's optional.
  4. Add a unit assertion on all three lengths at your batch producer.

Example fix

# before
index.index_batch(ids, texts, metadata=uniq_metadata)  # 10 ids, 7 metadata -> ValueError

# after
meta_by_id = dict(zip(ids, uniq_metadata_list))
index.index_batch(ids, texts, metadata=[meta_by_id.get(mid, {}) for mid in ids])
Defensive patterns

Strategy: validation

Validate before calling

ids = [m.id for m in memories]
texts = [m.text for m in memories]
meta = [m.metadata or {} for m in memories]  # derived from the SAME iterable
assert len(ids) == len(texts) == len(meta)

Try / catch

try:
    index.index_batch(ids, texts, metadata=meta)
except ValueError as e:
    if "must match memory_ids" in str(e):
        meta = [meta_by_id.get(mid, {}) for mid in ids]  # rebuild aligned
        index.index_batch(ids, texts, metadata=meta)
    else:
        raise

Prevention

When it happens

Trigger: Calling index_batch(ids, texts, metadata=...) where metadata was built per-text, per-chunk, per-unique-record, or filtered independently — any producer whose count diverges from len(memory_ids).

Common situations: Deduplicating metadata but not ids; metadata computed only for successful lookups (missing key skipped silently); chunked texts with metadata per original doc; reusing a cached metadata list after the id list changed.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/a578fd287aa94e83. Report an issue: GitHub.