Skip to content

feat(live-runner): session-owned payment lifecycle for streamed sessions#31

Draft
rickstaa wants to merge 3 commits into
ja/live-runnerfrom
rs/live-runner-session-payments
Draft

feat(live-runner): session-owned payment lifecycle for streamed sessions#31
rickstaa wants to merge 3 commits into
ja/live-runnerfrom
rs/live-runner-session-payments

Conversation

@rickstaa

@rickstaa rickstaa commented Jun 17, 2026

Copy link
Copy Markdown
Member

Draft for review and comparison against Josh's ja/live-runner branch.

What this adds

Streamed live-runner sessions (trickle / websocket) must keep paying for as long as the session is open. After the reservation 402, go-livepeer holds the session as a prepaid balance: ReserveLiveRunnerSession (server/ai_http.go) spawns a server-side ticker (-livePaymentInterval, 5s default) that debits the balance and silently releases the session once it runs dry. There is no further 402; the client must keep crediting out of band via POST /payment. Unlike call_runner (request/response, where the orchestrator pulls payment via a 402 inline), a held-open transport has no request to attach payment to, so the client pushes on a cadence below the server tick.

This makes LiveRunnerSession own that lifecycle so callers do not hand-manage a background task.

Commits

  • 3b60852 surface payment session and add run_session_payments (the interval payment loop).
  • 37ee769 make the session own start/stop of that loop.
  • 81ee7be handle the 482 skip-payment gate and anchor the interval to the server tick.

Changes

  • LiveRunnerSession (still @dataclass(frozen=True)) gains start_payments(), aclose(), and async context manager support. _payment_task is stored via object.__setattr__, mirroring Lv2vJob.start_payment_sender / close. start_payments is idempotent, a no-op offchain, and warns instead of raising when called without a running loop.
  • run_session_payments pays immediately before the first sleep (a cold start can leave a long gap after the reservation payment), treats HTTP 482 / SkipPaymentCycle as a healthy "balance current" gate (debug, keep looping) rather than a failure, and logs and retries other per-cycle failures instead of dying.
  • reserve_session threads payment_interval through to the session (default 3s, margin under the 5s server tick).

Design notes vs lv2v and scope

  • Uses the transport-agnostic interval driver rather than lv2v's per-output-segment sender. The general live-runner path has no single output stream to meter, can create multiple app-defined trickle channels, and must also cover websocket (no segments). The orchestrator meters by time anyway (LivePaymentProcessor on livePaymentInterval), so interval push aligns more directly with the server than per-segment does.
  • Payment delivery is out of band (send_payment POSTs to {orch}/payment), so one interval loop funds any held-open transport.

Usage

# automatic lifecycle
async with await reserve_session(app=APP, signer_url=signer) as session:
    channels = await create_trickle_channels(session.session_id, [...])
    # publish/subscribe; payments start on enter, stop on exit

# or manual (lv2v parity)
session = await reserve_session(app=APP, signer_url=signer)
session.start_payments()
try:
    ...
finally:
    await session.aclose()

Tests

tests/test_live_runner_payments.py (9 cases): offchain no-op, immediate first payment, survives a transient payment error, treats a skip cycle as paid-up, idempotency, no-loop skip, aclose, async context manager. Full suite: 18 passed, ruff clean.

Scope and altitude (intentionally left for Josh)

This PR is faithful to the payment-loop mechanics in scope (LivepeerClient._payment_loop) and lv2v (Lv2vJob), and to the go-livepeer server model. It is intentionally scoped to the payment lifecycle and leaves the larger object-shape decisions open:

  1. Object home. scope's LivepeerClient.connect() owns three connection-lifetime loops together: _payment_loop, _events_loop, _ping_loop. Here only payments live on the (thin) LiveRunnerSession. If the SDK should grow a scope-style LiveRunnerClient that owns payments, events, and heartbeat over one connection lifecycle, these methods move into it unchanged. Open question: is payment lifecycle on LiveRunnerSession the intended surface, or a stepping stone to a unified client?
  2. Silent-drop detection. The orchestrator releases an underfunded session with no error on the payment path; the way to learn that is the events channel (scope's _events_loop). Not handled here. A client true to scope would watch events and fail fast instead of discovering the drop via dead media.
  3. Surface the cadence. The interval is currently a client-side default (3s) chosen to sit under the server's 5s livePaymentInterval. The reservation challenge does not surface that value (only payment_params / orchestrator / manifest_id), so the client guesses. Surfacing livePaymentInterval in the challenge would let the client self-tune, which is an orchestrator-side change.
  4. Async context manager. The async with sugar is an addition beyond scope and lv2v (both use explicit start/close); kept because reserve_session is async-native. Drop it if you prefer to match the existing explicit pattern.
  5. Demand-driven streamed payments (bigger). A true "pay when the orchestrator requests" model (like the 402 path, but off the media hot path, via a payment-due signal over the control or events channel, since a mid-stream 402 on trickle segments adds jitter and is impossible over websocket) would let the interval disappear entirely for streamed sessions. That is a go-livepeer protocol change, not an SDK one.

Relates to ENG-130. Keeping this as a draft for Josh to take the object-shape pieces.

🤖 Generated with Claude Code

rickstaa and others added 2 commits June 15, 2026 20:40
reserve_session now returns the LivePaymentSession built during the reserve
payment challenge (previously discarded). Add run_session_payments(session),
a timer-driven loop that calls LivePaymentSession.send_payment on an interval
to keep a long-lived session funded — the orchestrator meters open sessions by
wall-clock time and releases them when the balance runs dry. No-op offchain.

Reusable across any held-open transport (trickle today, websockets next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a session-owned payment lifecycle to LiveRunnerSession so streamed
(trickle/websocket) sessions stay funded without the caller hand-managing
a background task. Mirrors the lv2v Lv2vJob.start_payment_sender/close
shape, but uses the transport-agnostic interval driver (run_session_payments)
since the general live-runner path has no single output stream to meter and
must also cover websocket.

- LiveRunnerSession (still frozen) gains start_payments(), aclose(), and
  async context manager support; _payment_task stored via object.__setattr__.
  start_payments is idempotent, a no-op offchain, and warns instead of raising
  when called without a running loop.
- run_session_payments now pays immediately before the first sleep (a cold
  start can leave a long gap after the reservation payment) and logs+retries
  per-cycle failures instead of dying.
- reserve_session threads payment_interval through to the session.

Callers can now do `async with await reserve_session(...) as session:` and get
automatic start/stop, or use start_payments()/aclose() manually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 17, 2026

Copy link
Copy Markdown

ENG-130

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5866795-03c9-4df6-9ba6-206ecbcf4bc6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rs/live-runner-session-payments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…o server tick

Refinements from reviewing the go-livepeer orchestrator design (ai_http.go
ReserveLiveRunnerSession): after the reservation 402, the orchestrator holds
the session as a prepaid balance debited by a server-side ticker
(-livePaymentInterval, 5s default) and silently releases it when underfunded.
The client just keeps crediting out-of-band, which is what run_session_payments
already does. Two corrections:

- Treat HTTP 482 / SkipPaymentCycle as a healthy "balance current" gate (debug,
  keep looping) instead of logging it as a payment failure. The orchestrator
  uses it to prevent overpayment; only genuine errors warn.
- Document that payment_interval must stay at or below the orchestrator's
  livePaymentInterval (5s), which is why the 3s default carries margin.

Add a test asserting a skip cycle does not kill the loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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