MemPalace/mempalace · error · ValueError

limit must be a positive integer

Error message

limit must be a positive integer

What it means

list_events (and wait_events, which forwards its filters) requires `limit` to be an int >= 1. Zero, negative numbers, floats like 10.0, and None all fail. Valid limits are then clamped to MAX_LIST_LIMIT = 500, so any positive integer works but is capped. bool is an int subclass, so True passes as limit=1 — usually a latent bug rather than intent.

Source

Thrown at mempalace/logstream.py:712

          event in append order (rowid), regardless of timestamp ties.
        - ``since_created_at`` is inclusive (``>=``) so second-granularity
          timestamps never skip events; callers dedup by ``id``.
        - ``to_agent`` also matches broadcast events (``to_agent='*'``).
        """
        stream = _sanitize_routing(stream, "stream", required=False)
        room = _sanitize_routing(room, "room", required=False)
        if type not in (None, ""):
            type = _sanitize_event_type(type)
        else:
            type = None
        to_agent = _sanitize_routing(to_agent, "to_agent", required=False)
        from_agent = _sanitize_routing(from_agent, "from_agent", required=False)
        correlation_id = _sanitize_routing(correlation_id, "correlation_id", required=False)
        status = _sanitize_status(status)
        since_event_id = _sanitize_routing(since_event_id, "since_event_id", required=False)
        since_created_at = sanitize_iso_temporal(since_created_at, "since_created_at") or None
        if not isinstance(limit, int) or limit < 1:
            raise ValueError("limit must be a positive integer")
        limit = min(limit, MAX_LIST_LIMIT)

        where = []
        params = []
        for column, value in (
            ("stream", stream),
            ("room", room),
            ("type", type),
            ("from_agent", from_agent),
            ("correlation_id", correlation_id),
            ("status", status),
        ):
            if value is not None:
                where.append(f"{column} = ?")
                params.append(value)
        if to_agent is not None:
            where.append("(to_agent = ? OR to_agent = '*')")
            params.append(to_agent)

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Pass a positive int: limit=50 (the DEFAULT_LIST_LIMIT) or anything 1..500.
  2. For 'as much as possible' use limit=500 (MAX_LIST_LIMIT) and paginate with since_event_id.
  3. Guard computed values: limit=max(1, min(int(limit or 50), 500)).

Example fix

// before
evts = ls.list_events(stream="project/x", limit=len(rooms))  # 0 when empty
// after
limit = max(1, min(len(rooms) or 50, 500))
evts = ls.list_events(stream="project/x", limit=limit)
Defensive patterns

Strategy: validation

Validate before calling

def safe_limit(limit, default=50, cap=500):
    if limit is None:
        return default
    return max(1, min(int(limit), cap))

limit = safe_limit(raw_limit)

Type guard

def is_valid_limit(v) -> bool:
    return isinstance(v, int) and not isinstance(v, bool) and v >= 1

Try / catch

try:
    evts = ls.list_events(stream=s, limit=limit)
except ValueError as e:
    if "limit must be a positive integer" in str(e):
        evts = ls.list_events(stream=s, limit=50)
    else:
        raise

Prevention

When it happens

Trigger: list_events(limit=0); limit=-1 to mean 'all'; limit=None when no default applied; limit=50.0 from a config float; limit computed as len(items) when the list is empty.

Common situations: Config files parsed with float values; callers using 0 as a sentinel for 'no limit'; a computed limit that can be 0 for empty result sets; JSON input where the field is optional and arrives as null.

Related errors


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