MemPalace/mempalace · error · ValueError

status={value!r} is not one of: {allowed}

Error message

status={value!r} is not one of: {allowed}

What it means

The `status` field of append_event/ack_event must be one of the fixed lifecycle set EVENT_STATUSES = {'open','claimed','ready','applied','blocked','failed','superseded'} (or None/'' to omit). Any other string — including near-misses like 'done', 'OK', 'claimed ' with a space — is rejected with the allowed list echoed in the message. Statuses are a controlled vocabulary so agents can filter reliably.

Source

Thrown at mempalace/logstream.py:137

def _sanitize_event_type(value) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ValueError("type must be a non-empty string")
    value = value.strip()
    if not _EVENT_TYPE_RE.match(value):
        raise ValueError(
            f"type={value!r} is not a valid event type "
            "(lowercase letters, digits, '.', '_', '-'; max 64 chars)"
        )
    return value


def _sanitize_status(value) -> Optional[str]:
    if value is None or value == "":
        return None
    if not isinstance(value, str) or value not in EVENT_STATUSES:
        allowed = ", ".join(sorted(EVENT_STATUSES))
        raise ValueError(f"status={value!r} is not one of: {allowed}")
    return value


def _sanitize_body(value, max_bytes: int, field_name: str = "body") -> str:
    """Validate verbatim payload text. Empty is allowed; ``None`` becomes ''."""
    if value is None:
        return ""
    if not isinstance(value, str):
        raise ValueError(f"{field_name} must be a string")
    if "\x00" in value:
        raise ValueError(f"{field_name} contains null bytes")
    value = strip_lone_surrogates(value)
    size = len(value.encode("utf-8"))
    if size > max_bytes:
        raise ValueError(f"{field_name} is {size} bytes; maximum is {max_bytes} bytes")
    return value

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Use one of the exact allowed values: open, claimed, ready, applied, blocked, failed, superseded.
  2. Map external statuses to the enum before calling (e.g. 'Closed' -> 'applied', 'In Review' -> 'claimed').
  3. Pass status=None or omit it when there is no lifecycle state to record.

Example fix

// before
ls.append_event(type="task.request", status="Done", ...)
// after
ls.append_event(type="task.request", status="applied", ...)
Defensive patterns

Strategy: validation

Validate before calling

from mempalace.logstream import EVENT_STATUSES

def normalize_status(s):
    if s is None or s == "":
        return None
    s = str(s).strip().lower()
    aliases = {"done": "applied", "ok": "applied", "in_progress": "claimed", "closed": "applied"}
    return aliases.get(s, s) if s in EVENT_STATUSES or s in aliases else None

status = normalize_status(raw_status)
if raw_status and status is None:
    raise ValueError(f"unmappable status {raw_status!r}")

Type guard

from mempalace.logstream import EVENT_STATUSES

def is_valid_status(s) -> bool:
    return s is None or s == "" or (isinstance(s, str) and s in EVENT_STATUSES)

Try / catch

try:
    evt = ls.append_event(..., status=status)
except ValueError as e:
    if "not one of" in str(e) and "status=" in str(e):
        evt = ls.append_event(..., status=None)  # drop rather than fail the event
    else:
        raise

Prevention

When it happens

Trigger: append_event(status='done'), status='In Progress', status='claimed!' from an LLM-generated payload, or ack_event(status='ok'). Passing a non-string such as status=0 or status=True also triggers it (isinstance check comes second).

Common situations: LLM agents freestyle a status word instead of the enum; callers mapping tickets from external trackers (Jira 'In Review', 'Closed') directly into the logstream; casing or spelling drift between producer versions.

Related errors


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/c393fcabd34c905c. Report an issue: GitHub.