Skip to content

feat(api): GET /events accepts since and limit query params with X-EXO-Last-Idx header#2133

Open
5F0jd2vLq54RerYW wants to merge 3 commits into
exo-explore:mainfrom
5F0jd2vLq54RerYW:pr/events-cursor
Open

feat(api): GET /events accepts since and limit query params with X-EXO-Last-Idx header#2133
5F0jd2vLq54RerYW wants to merge 3 commits into
exo-explore:mainfrom
5F0jd2vLq54RerYW:pr/events-cursor

Conversation

@5F0jd2vLq54RerYW
Copy link
Copy Markdown

Summary

Extends GET /events to support cursor-based tailing without polling the full /state snapshot.

Changes:

  • src/exo/api/main.py: Add optional since (default 0) and limit (default None) query params to stream_events(). Takes a fast-path to read_all() when both are absent — identical to pre-patch behavior.
  • src/exo/api/tests/test_stream_events.py: 7 test cases covering full-dump backward compat, since+limit, since-only, since-beyond-count (empty), limit-clamped, negative since rejected (FastAPI ge=0), and chained cursor reads with no overlap.
  • src/exo/api/tests/conftest.py: sys.modules stub for exo_rs so API tests run without requiring a compiled Rust extension.

Response header: X-EXO-Last-Idx: <N> is set to the upper exclusive bound of the range consumed, allowing clients to chain reads with no gap and no overlap.

Motivation

Downstream tooling needs to detect state changes efficiently. The previous approach required polling the full /state snapshot (~150KB). With this change, a client can tail events incrementally:

cursor = 0
while True:
    resp = requests.get(f"http://localhost:52415/events?since={cursor}")
    events = resp.json()
    cursor = int(resp.headers["X-EXO-Last-Idx"])
    process(events)
    sleep(1)

Backward Compatibility

GET /events with no query params takes the same read_all() fast-path as before. No existing client behavior changes.

🤖 Generated with Claude Code

Michael Mei and others added 3 commits May 31, 2026 17:05
…st-Idx header

Extends GET /events stream_events handler to accept optional since (default 0)
and limit (default None) query parameters. Reuses the existing
DiskEventLog.read_range(start, end) primitive, so cost is ~10 LOC of API
surface plus a test.

The response includes an X-EXO-Last-Idx header set to the upper bound
consumed, allowing clients to chain reads without a separate /state
round-trip for lastEventAppliedIdx. Backward compatible: no params (since=0,
limit=None) takes a fast path that calls read_all() and matches pre-patch
behavior exactly.

Use case: enables event-cursor tailing for downstream tools like the
control-plane EXO swarm dispatcher, which previously had to poll the full
~150KB /state snapshot to detect runner state changes.

Test: src/exo/api/tests/test_stream_events.py — 7 cases covering full dump
backward compat, since+limit, since-only, since-beyond-count (empty),
limit-larger-than-remaining (clamped), negative since rejected (FastAPI
ge=0 validator), and chained cursor reads with no overlap.

Patch is intended for upstream PR to exo-explore/exo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vents tests

Adds src/exo/api/tests/conftest.py with a sys.modules stub for the exo_rs
Rust extension so Python-level API tests can run without a compiled binary.
The stub provides empty placeholder classes for FromSwarm, AllQueuesFullError,
MessageTooLargeError, NoPeersSubscribedToTopicError, Keypair, NetworkingHandle,
Pidfile, and PidfileError — covering all exo_rs symbols imported at module level
by exo.routing and exo.main. Only installed when the real extension is absent.

Also removes unused `pytest` import from test_stream_events.py (ruff F401).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@5F0jd2vLq54RerYW
Copy link
Copy Markdown
Author

Note: nix is not available in this dev environment, so I ran ruff format (which is what nix fmt invokes for Python files per the treefmt config in flake.nix) and pushed the formatting commit. The Rust/Svelte/TOML formatters don't apply to this PR (Python-only change).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant