Skip to content

feat(devbox): add devbox.on_evict eviction notifications (SSE)#824

Open
jrvb-rl wants to merge 6 commits into
release-please--branches--main--changes--nextfrom
feat/devbox-on-evict-sse
Open

feat(devbox): add devbox.on_evict eviction notifications (SSE)#824
jrvb-rl wants to merge 6 commits into
release-please--branches--main--changes--nextfrom
feat/devbox-on-evict-sse

Conversation

@jrvb-rl

@jrvb-rl jrvb-rl commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Description

Follow-on to the server-side flex eviction-notification work (monorepo #10325 and its successor SSE endpoint). Adds the object-oriented client surface for eviction notifications.

New API: devbox.on_evict(callback) (and devbox.cancel_on_evict()), on both the sync Devbox and async AsyncDevbox.

How it works (internal state management):

  • A per-client EvictionMonitor (sdk/eviction.py, async sdk/async_eviction.py) holds the interest set: {devbox_id -> callback}. Monitors are looked up via a weak client -> monitor registry (monitor_for), so every Devbox built from the same generated client shares one monitor and thus one SSE connection. No Devbox/factory constructor signatures changed.
  • The first on_evict across any devbox opens a single account-wide devboxes.watch_evictions() SSE stream on a background thread (sync) / asyncio.Task (async).
  • On each event: the id is matched against the interest set; unknown ids are discarded. The devbox is removed before its callback runs, so it fires at most once even if the server repeats a notification.
  • When the interest set empties, the stream is closed. Re-registering re-opens it.
  • The server replays all currently-pending evictions on connect, so late registrants are still notified.
  • RunloopSDK.close() / AsyncRunloopSDK.aclose() tear the monitor down.

Dependency / status

⚠️ Draft. This depends on the Stainless-generated devboxes.watch_evictions method and the DevboxEvictionEvent type (devbox_id, eviction_deadline_ms), which are generated downstream once the server endpoint lands. Those symbols are imported under TYPE_CHECKING and referenced by assumed name — they will be reconciled, and tests + an example added, once generation lands. No generated code was modified.

Testing

Deferred until the generated endpoint exists (see above). New/edited files byte-compile cleanly.

🤖 Generated with Claude Code

@reflex-loop

reflex-loop Bot commented Jul 23, 2026

Copy link
Copy Markdown

🔄 Reflex agent status: Working

The agent is working on this pull request.

This PR was created by Reflex.

View the agent run →

This comment updates in place as the agent works.

@jrvb-rl

jrvb-rl commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Reconciled with the endpoint as landed:

  • callback is now callback(devbox, eviction_deadline_ms) (the Devbox object + Unix-ms deadline)
  • uses the real generated type DevboxEvictionEventView

Ordering: depends on runloopai/runloop#10404, which marks watch_evictions as streaming in openapi.stainless.yml so Stainless generates devboxes.watch_evictions() -> Stream[DevboxEvictionEventView]. Once that merges and Stainless regenerates (the .stats.yml bump lands the method + type), this rebases green.

stainless-app Bot and others added 2 commits July 24, 2026 00:08
Adds an object-oriented `Devbox.on_evict(callback)` (sync and async) backed by a
per-client `EvictionMonitor`. The first registration opens a single account-wide
`watch_evictions` SSE stream; incoming notifications are matched against the local
interest set, the devbox is removed before its callback fires (at-most-once), and
the stream is closed once the interest set empties.

Depends on the Stainless-generated `devboxes.watch_evictions` method and
`DevboxEvictionEvent` type, which land downstream once the server endpoint merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jrvb-rl
jrvb-rl changed the base branch from main to release-please--branches--main--changes--next July 24, 2026 09:33
@jrvb-rl
jrvb-rl force-pushed the feat/devbox-on-evict-sse branch from 1c62073 to 1e699b8 Compare July 24, 2026 09:33
- Use the real generated event type DevboxEvictionEventView (the endpoint's view)
  in place of the assumed DevboxEvictionEvent.
- Invoke the callback as callback(devbox, eviction_deadline_ms): the Devbox object
  and the Unix millisecond deadline, instead of passing the raw event.

Requires the main-repo stainless config change that generates watch_evictions as
Stream[DevboxEvictionEventView] (runloopai/runloop#10404).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jrvb-rl
jrvb-rl force-pushed the feat/devbox-on-evict-sse branch from 1e699b8 to ffdd21b Compare July 24, 2026 09:37
@jrvb-rl
jrvb-rl marked this pull request as ready for review July 24, 2026 09:58
@jrvb-rl
jrvb-rl requested review from james-rl and sid-rl July 24, 2026 10:02
@stainless-app
stainless-app Bot force-pushed the release-please--branches--main--changes--next branch from 0ebcf9d to 92a2cfe Compare July 24, 2026 10:04
claude added 2 commits July 24, 2026 11:08
The mux SSE route moved from /v1/devboxes/watch_evictions to
/v1/devboxes/evictions/watch (runloopai/runloop#10409) to avoid colliding
with GET /v1/devboxes/{id}. Update the generated request path so the
watch_evictions method reaches the new endpoint. Method surface unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs made devbox.on_evict never fire:

1. The generated watch_evictions() sends Accept: application/json, but the
   endpoint only streams for Accept: text/event-stream — otherwise it returns an
   empty text/plain 200. The monitor now forces the SSE Accept header.
2. The monitor ran the stream once and stopped. The server force-closes on
   leader change / slow consumer (and a long-lived HTTP/2 stream can drop), and
   expects the client to reconnect and re-read the snapshot. The monitor now
   reconnects with backoff until no devbox is still interested.

Adds debug logging around connect / event / reconnect. Verified against dev: a
forced flex drain now delivers on_evict for 20/20 devboxes before suspend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gautam-rl

Copy link
Copy Markdown
Contributor

Async parity gap

EvictionMonitor (sync) got the Accept + reconnect fix, but AsyncEvictionMonitor still has the pre-fix _run:

  • calls watch_evictions() with no extra_headers={"Accept": "text/event-stream"}
  • runs the stream once and exits (no backoff/reconnect)

That reintroduces the silent “never fires” failure for AsyncDevbox.on_evict — same root causes called out in the sync fix commit.

Please mirror the sync monitor’s connect/reconnect loop in sdk/async_eviction.py before merge.

Addresses review on #824: AsyncEvictionMonitor still had the pre-fix single-shot
_run, so AsyncDevbox.on_evict would silently never fire. Port the sync monitor's
fixes to sdk/async_eviction.py:
- request the SSE Accept header (text/event-stream) on watch_evictions
- reconnect with capped backoff until no devbox is still interested

Also ruff-format sdk/eviction.py (the sync fix wrapped a call that fits on one line).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jrvb-rl

jrvb-rl commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the async parity gap in 7893f59: AsyncEvictionMonitor._run now mirrors the sync monitor — it forces the Accept: text/event-stream header on watch_evictions and reconnects with capped backoff (asyncio.sleep) until no devbox is still interested, exiting quietly on intentional teardown. asyncio.CancelledError propagates cleanly. (Also ruff-formatted sdk/eviction.py, which had a wrapped call that fits on one line.)

Note: the remaining test_devboxes.py::*watch_evictions* failures are unrelated to this wrapper — they're the /v1/devboxes/evictions/watch rename: the generated method targets the new server path, but the bundled OpenAPI spec + prism mock are still on the old path from the pre-rename Stainless regen. They resolve once Stainless regenerates against the current runloop openapi.json.

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.

4 participants