{"record":{"id":"ce4eb1664d0f2b29","repo":"MemPalace/mempalace","slug":"metadata-must-be-an-object","errorCode":null,"errorMessage":"metadata must be an object","messagePattern":"metadata must be an object","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"mempalace/logstream.py","lineNumber":161,"sourceCode":"    if value is None:\n        return \"\"\n    if not isinstance(value, str):\n        raise ValueError(f\"{field_name} must be a string\")\n    if \"\\x00\" in value:\n        raise ValueError(f\"{field_name} contains null bytes\")\n    value = strip_lone_surrogates(value)\n    size = len(value.encode(\"utf-8\"))\n    if size > max_bytes:\n        raise ValueError(f\"{field_name} is {size} bytes; maximum is {max_bytes} bytes\")\n    return value\n\n\ndef _sanitize_metadata(value) -> str:\n    \"\"\"Validate optional metadata dict and return its canonical JSON text.\"\"\"\n    if value is None:\n        return \"{}\"\n    if not isinstance(value, dict):\n        raise ValueError(\"metadata must be an object\")\n    try:\n        encoded = json.dumps(value, ensure_ascii=False, sort_keys=True)\n    except (TypeError, ValueError) as exc:\n        raise ValueError(f\"metadata is not JSON-serializable: {exc}\") from None\n    if len(encoded.encode(\"utf-8\")) > MAX_METADATA_BYTES:\n        raise ValueError(f\"metadata exceeds maximum size of {MAX_METADATA_BYTES} bytes\")\n    return encoded\n\n\nclass Logstream:\n    \"\"\"Durable append-only coordination log (events + artifacts).\n\n    Storage and threading mirror ``KnowledgeGraph``: one SQLite file in\n    WAL mode, a per-instance lock around writes, ``check_same_thread=False``\n    so the MCP HTTP server can call from worker threads.\n    \"\"\"\n\n    def __init__(","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/logstream.py#L143-L179","documentation":"The `metadata` argument to append_event or put_artifact must be a Python dict (or None, which becomes '{}') because it is serialized to canonical sorted-key JSON and stored in a text column. Lists, strings, and scalars are rejected before serialization. The metadata is meant to be a flat structured annotation object.","triggerScenarios":"append_event(metadata='[1,2]') (string); metadata=[('k','v')] (list of tuples); metadata='ok'; put_artifact(metadata=json_string) — passing pre-encoded JSON text instead of the dict itself.","commonSituations":"Callers double-serializing (json.dumps then passing the string); handlers forwarding a JSON array from an MCP request; teams migrating from a schema where metadata was a string blob.","solutions":["Pass a plain dict: metadata={'priority': 1, 'source': 'hub'}.","If you received JSON text, parse it first: metadata=json.loads(raw) and confirm it is a dict.","Use metadata=None (or omit) when there is nothing to attach."],"exampleFix":"// before\nevt = ls.append_event(..., metadata=\"{\\\"priority\\\": 1}\")\n// after\nimport json\nevt = ls.append_event(..., metadata=json.loads(\"{\\\"priority\\\": 1}\"))","handlingStrategy":"type-guard","validationCode":"def as_metadata(value):\n    if value is None:\n        return None\n    if isinstance(value, str):\n        value = json.loads(value)  # accept pre-encoded JSON\n    if not isinstance(value, dict):\n        raise TypeError(\"metadata must be a dict\")\n    return value\n\nmetadata = as_metadata(raw_metadata)","typeGuard":"def is_valid_metadata(m) -> bool:\n    return m is None or isinstance(m, dict)","tryCatchPattern":"try:\n    evt = ls.append_event(..., metadata=metadata)\nexcept ValueError as e:\n    if \"must be an object\" in str(e):\n        evt = ls.append_event(..., metadata={\"value\": metadata})  # wrap scalars\n    else:\n        raise","preventionTips":["Pass dicts straight through; never json.dumps before the call — the library serializes canonically itself.","json.loads any inbound JSON text at the system boundary and assert the result is a dict."],"tags":["validation","logstream","metadata","type-error"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}