MemPalace/mempalace · error · ValueError

after_seq must be a non-negative integer

Error message

after_seq must be a non-negative integer

What it means

list_ops(origin, after_seq, limit) — the anti-entropy pull unit — requires after_seq to be an int >= 0. It is an exclusive sequence cursor into that origin's authored events (origin_seq > after_seq), so negatives, floats, strings, and None are rejected. Use get_replica_tops() to learn each replica's current top sequence before pulling.

Source

Thrown at mempalace/logstream.py:824

        """{origin_replica: highest origin_seq applied locally}.

        The complete description of this replica's knowledge — peers diff
        their vectors to compute exactly which op ranges are missing.
        """
        with self._lock:
            conn = self._conn()
            rows = conn.execute(
                "SELECT origin_replica, max(origin_seq) AS top FROM events "
                "WHERE origin_replica IS NOT NULL GROUP BY origin_replica"
            ).fetchall()
        return {row["origin_replica"]: row["top"] for row in rows}

    def list_ops(self, origin: str, after_seq: int = 0, limit: int = 500) -> list[dict]:
        """Events authored by ``origin`` with origin_seq > after_seq, in
        author order. The anti-entropy pull unit."""
        origin = _sanitize_routing(origin, "origin")
        if not isinstance(after_seq, int) or after_seq < 0:
            raise ValueError("after_seq must be a non-negative integer")
        if not isinstance(limit, int) or limit < 1:
            raise ValueError("limit must be a positive integer")
        limit = min(limit, MAX_LIST_LIMIT)
        with self._lock:
            conn = self._conn()
            rows = conn.execute(
                "SELECT rowid, * FROM events WHERE origin_replica = ? AND origin_seq > ? "
                "ORDER BY origin_seq ASC LIMIT ?",
                (origin, after_seq, limit),
            ).fetchall()
            events = [self._event_dict(row) for row in rows]
            return self._attach_artifact_ids(conn, events)

    def apply_remote_event(self, event: dict) -> bool:
        """Fold one remote op into the local log, idempotently.

        Verbatim rule: the event is stored exactly as authored (id,
        created_at, hlc, origin stamps untouched); only the local rowid —

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Pass a plain int >= 0; on first pull use the default after_seq=0.
  2. Persist the cursor as int and coerce on load: after_seq=int(cursor or 0).
  3. Feed it from the remote's state: tops = remote.get_replica_tops(); list_ops('mac-codex', after_seq=tops.get('mac-codex', 0)).

Example fix

// before
ops = ls.list_ops("mac-codex", after_seq=state.get("cursor"))  # None / str
// after
ops = ls.list_ops("mac-codex", after_seq=int(state.get("cursor") or 0))
Defensive patterns

Strategy: validation

Validate before calling

def safe_after_seq(value) -> int:
    return max(0, int(value or 0))

after_seq = safe_after_seq(state.get("cursor"))

Type guard

def is_valid_after_seq(v) -> bool:
    return isinstance(v, int) and not isinstance(v, bool) and v >= 0

Try / catch

try:
    ops = ls.list_ops(origin, after_seq=after_seq)
except ValueError as e:
    if "after_seq" in str(e):
        ops = ls.list_ops(origin, after_seq=0)  # cursor lost; re-pull from start
    else:
        raise

Prevention

When it happens

Trigger: list_ops('mac-codex', after_seq=-1); after_seq='120' from a config string; after_seq=None when no cursor was loaded; after_seq=120.0 float from a JSON round-trip.

Common situations: Sync clients persisting cursors as strings in a state file; first-pull code that forgot the default 0; cursors loaded from JSON where large ints became floats in another language.

Related errors


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