{"record":{"id":"95e28bb77de8d966","repo":"MemPalace/mempalace","slug":"after-seq-must-be-a-non-negative-integer","errorCode":null,"errorMessage":"after_seq must be a non-negative integer","messagePattern":"after_seq must be a non-negative integer","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"mempalace/logstream.py","lineNumber":824,"sourceCode":"        \"\"\"{origin_replica: highest origin_seq applied locally}.\n\n        The complete description of this replica's knowledge — peers diff\n        their vectors to compute exactly which op ranges are missing.\n        \"\"\"\n        with self._lock:\n            conn = self._conn()\n            rows = conn.execute(\n                \"SELECT origin_replica, max(origin_seq) AS top FROM events \"\n                \"WHERE origin_replica IS NOT NULL GROUP BY origin_replica\"\n            ).fetchall()\n        return {row[\"origin_replica\"]: row[\"top\"] for row in rows}\n\n    def list_ops(self, origin: str, after_seq: int = 0, limit: int = 500) -> list[dict]:\n        \"\"\"Events authored by ``origin`` with origin_seq > after_seq, in\n        author order. The anti-entropy pull unit.\"\"\"\n        origin = _sanitize_routing(origin, \"origin\")\n        if not isinstance(after_seq, int) or after_seq < 0:\n            raise ValueError(\"after_seq must be a non-negative integer\")\n        if not isinstance(limit, int) or limit < 1:\n            raise ValueError(\"limit must be a positive integer\")\n        limit = min(limit, MAX_LIST_LIMIT)\n        with self._lock:\n            conn = self._conn()\n            rows = conn.execute(\n                \"SELECT rowid, * FROM events WHERE origin_replica = ? AND origin_seq > ? \"\n                \"ORDER BY origin_seq ASC LIMIT ?\",\n                (origin, after_seq, limit),\n            ).fetchall()\n            events = [self._event_dict(row) for row in rows]\n            return self._attach_artifact_ids(conn, events)\n\n    def apply_remote_event(self, event: dict) -> bool:\n        \"\"\"Fold one remote op into the local log, idempotently.\n\n        Verbatim rule: the event is stored exactly as authored (id,\n        created_at, hlc, origin stamps untouched); only the local rowid —","sourceCodeStart":806,"sourceCodeEnd":842,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/logstream.py#L806-L842","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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))."],"exampleFix":"// before\nops = ls.list_ops(\"mac-codex\", after_seq=state.get(\"cursor\"))  # None / str\n// after\nops = ls.list_ops(\"mac-codex\", after_seq=int(state.get(\"cursor\") or 0))","handlingStrategy":"validation","validationCode":"def safe_after_seq(value) -> int:\n    return max(0, int(value or 0))\n\nafter_seq = safe_after_seq(state.get(\"cursor\"))","typeGuard":"def is_valid_after_seq(v) -> bool:\n    return isinstance(v, int) and not isinstance(v, bool) and v >= 0","tryCatchPattern":"try:\n    ops = ls.list_ops(origin, after_seq=after_seq)\nexcept ValueError as e:\n    if \"after_seq\" in str(e):\n        ops = ls.list_ops(origin, after_seq=0)  # cursor lost; re-pull from start\n    else:\n        raise","preventionTips":["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."],"tags":["validation","logstream","sync","cursor"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}