{"record":{"id":"a578fd287aa94e83","repo":"headroomlabs-ai/headroom","slug":"metadata-len-metadata-must-match-memory-ids","errorCode":null,"errorMessage":"metadata ({len(metadata)}) must match memory_ids ({len(memory_ids)}) length","messagePattern":"metadata \\((.+?)\\) must match memory_ids \\((.+?)\\) length","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"headroom/memory/adapters/fts5.py","lineNumber":167,"sourceCode":"        metadata: list[dict] | None = None,\n    ) -> None:\n        \"\"\"Index multiple memories in a single transaction.\n\n        Args:\n            memory_ids: List of unique identifiers.\n            texts: List of text contents to index.\n            metadata: Optional list of metadata dicts (one per memory).\n\n        Raises:\n            ValueError: If memory_ids and texts have different lengths.\n        \"\"\"\n        if len(memory_ids) != len(texts):\n            raise ValueError(\n                f\"memory_ids ({len(memory_ids)}) and texts ({len(texts)}) must have same length\"\n            )\n\n        if metadata is not None and len(metadata) != len(memory_ids):\n            raise ValueError(\n                f\"metadata ({len(metadata)}) must match memory_ids ({len(memory_ids)}) length\"\n            )\n\n        metadata = metadata or [{} for _ in memory_ids]\n\n        with self._get_conn() as conn:\n            # Delete existing entries\n            conn.executemany(\n                \"DELETE FROM memory_fts WHERE memory_id = ?\",\n                [(mid,) for mid in memory_ids],\n            )\n\n            # Prepare batch data\n            batch_data = []\n            for memory_id, text, meta in zip(memory_ids, texts, metadata):\n                user_id = meta.get(\"user_id\", \"\")\n                session_id = meta.get(\"session_id\", \"\")\n                category = \"\"  # Deprecated - kept for backwards compatibility","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/memory/adapters/fts5.py#L149-L185","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Compare the counts in the message; regenerate metadata with a list comprehension over memory_ids so lengths lock together.","If metadata is unknown for some rows, pad with empty dicts: metadata=[m.get(mid, {}) for mid in memory_ids].","Omit the metadata argument entirely when you don't have per-id data — it's optional.","Add a unit assertion on all three lengths at your batch producer."],"exampleFix":"# before\nindex.index_batch(ids, texts, metadata=uniq_metadata)  # 10 ids, 7 metadata -> ValueError\n\n# after\nmeta_by_id = dict(zip(ids, uniq_metadata_list))\nindex.index_batch(ids, texts, metadata=[meta_by_id.get(mid, {}) for mid in ids])","handlingStrategy":"validation","validationCode":"ids = [m.id for m in memories]\ntexts = [m.text for m in memories]\nmeta = [m.metadata or {} for m in memories]  # derived from the SAME iterable\nassert len(ids) == len(texts) == len(meta)","typeGuard":null,"tryCatchPattern":"try:\n    index.index_batch(ids, texts, metadata=meta)\nexcept ValueError as e:\n    if \"must match memory_ids\" in str(e):\n        meta = [meta_by_id.get(mid, {}) for mid in ids]  # rebuild aligned\n        index.index_batch(ids, texts, metadata=meta)\n    else:\n        raise","preventionTips":["Derive all parallel lists from one source iterable of records.","Default missing metadata to {} per id rather than dropping entries.","Skip the metadata argument when data is incomplete instead of passing a short list."],"tags":["validation","batch","fts5","memory","invariant"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}