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
- Pass a plain int >= 0; on first pull use the default after_seq=0.
- Persist the cursor as int and coerce on load: after_seq=int(cursor or 0).
- 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
- Persist sync cursors as ints in your state file and int() them on load.
- Seed first pulls from get_replica_tops() rather than hand-written numbers.
- Treat a lost/corrupt cursor as after_seq=0 (full re-pull), which is safe because ops are idempotent to apply.
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
- {field_name} must be a non-empty string
- {field_name} exceeds maximum length of {_MAX_ROUTING_LENGTH}
- {field_name} contains control characters
- type must be a non-empty string
- type={value!r} is not a valid event type (lowercase letters,
AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15).
Data as JSON: /api/errors/95e28bb77de8d966.
Report an issue: GitHub.