{"record":{"id":"2e30e070b3bce84d","repo":"MemPalace/mempalace","slug":"content-must-be-a-non-empty-string","errorCode":null,"errorMessage":"content must be a non-empty string","messagePattern":"content must be a non-empty string","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"mempalace/config.py","lineNumber":213,"sourceCode":"\n    return value\n\n\ndef sanitize_iso_date(value, field_name: str = \"date\"):\n    \"\"\"Backward-compatible wrapper for ISO temporal validation.\n\n    Historically this accepted only full dates. It now also accepts canonical\n    UTC datetimes, but the old name is kept so existing imports continue to\n    work.\n    \"\"\"\n\n    return sanitize_iso_temporal(value, field_name)\n\n\ndef sanitize_content(value: str, max_length: int = 100_000) -> str:\n    \"\"\"Validate drawer/diary content length.\"\"\"\n    if not isinstance(value, str) or not value.strip():\n        raise ValueError(\"content must be a non-empty string\")\n    if len(value) > max_length:\n        raise ValueError(f\"content exceeds maximum length of {max_length} characters\")\n    if \"\\x00\" in value:\n        raise ValueError(\"content contains null bytes\")\n    return strip_lone_surrogates(value)\n\n\nDEFAULT_PALACE_PATH = os.path.expanduser(\"~/.mempalace/palace\")\nDEFAULT_COLLECTION_NAME = \"mempalace_drawers\"\nDEFAULT_BACKEND = \"chroma\"\nDEFAULT_MILVUS_CONSISTENCY_LEVEL = \"Strong\"\n_MILVUS_CONSISTENCY_LEVELS = {\n    \"strong\": \"Strong\",\n    \"session\": \"Session\",\n    \"bounded\": \"Bounded\",\n    \"eventually\": \"Eventually\",\n}\n","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/config.py#L195-L231","documentation":"Raised by sanitize_content() when drawer/diary content is not a string, or is empty/whitespace-only. Content is stored verbatim, so an empty payload would create a drawer that exists but holds nothing — the validator rejects it at the boundary. All drawer/diary write paths (CLI, MCP tools) route through this check.","triggerScenarios":"Calling a drawer/diary write API with content=None, content=123, or \"\" / \"   \" — e.g. a hook script passing an empty transcript buffer, or an MCP tool call where the content argument was omitted.","commonSituations":"Stop-hooks saving a session whose extracted text came back empty; scripts forwarding an unset variable; whitespace-only content from a failed extraction step; JSON payloads with a missing content key defaulting to None.","solutions":["Skip the write entirely when there is no content: `if not content or not content.strip(): return`","Fix the extraction/hook step that produced empty content instead of forcing a write","Pass content as a str; encode/decode binary input explicitly before the call"],"exampleFix":"# before\nsave_drawer(wing, room, transcript_text)   # may be \"\"\n\n# after\nif transcript_text and transcript_text.strip():\n    save_drawer(wing, room, transcript_text)","handlingStrategy":"validation","validationCode":"# Skip degenerate writes instead of letting the library reject them:\nif not isinstance(content, str) or not content.strip():\n    return  # nothing to store verbatim","typeGuard":"def is_nonempty_content(value) -> bool:\n    return isinstance(value, str) and bool(value.strip())","tryCatchPattern":"try:\n    safe = sanitize_content(content)\nexcept ValueError as exc:\n    if \"non-empty\" in str(exc):\n        return  # deliberately skip empty drawers\n    raise","preventionTips":["Make hook/extraction scripts no-op on empty text instead of forcing saves","Check transcripts for blank extraction results before writing","Pass content as str; never forward raw bytes"],"tags":["validation","content","verbatim","mempalace"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}