Skip to content

fix(video): clamp the last poll so the 9-min budget cannot outlive its authorization - #111

Merged
VickyXAI merged 1 commit into
mainfrom
fix/video-poll-clamp
Aug 28, 2026
Merged

VickyXAI merged 1 commit into
mainfrom
fix/video-poll-clamp

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #110. The 540s polling budget it introduced sits 60s inside the 600s payment authorization only at loop entry — the loop body then adds a 5s interval sleep and an unbounded 90s poll timeout, so a poll entered at 539.9s stays in flight until 635s, 35s past validBefore.

  • clamp the last poll to the remaining budget via pollTimeoutFor(), bounding worst-case wall time at exactly 540s
  • name VIDEO_POLL_TIMEOUT_MS and VIDEO_PAYMENT_AUTH_SECONDS, and use the latter where the authorization is signed (it was a bare 600 literal)
  • replace the constants-only budget assertion with the real invariant plus a swept property test

Why

validBefore = signingTime + maxTimeoutSeconds (@blockrun/llm/dist/index.js:109), and settlement happens server-side on the poll that answers "completed". A poll outliving the authorization fails settlement for a video that actually generated.

This is not hypothetical — blockrun_music shipped the same bug. From CHANGELOG:

fix(music) — keep the payment authorization valid through the poll window. ... A slow MiniMax track completing after validBefore failed settlement server-side and surfaced as a wrong "fund your wallet" error for a track that actually generated.

That fix raised music to 600s to stay inside the window; #110 pushed video back out past it.

budget +interval +poll timeout worst case vs 600s auth
video (before #110) 300s 5s 90s 395s +205s safe
music 240s 5s 90s 335s +265s safe
video (after #110) 540s 5s 90s 635s -35s expired
video (this PR) 540s 5s clamped 540s +60s safe

There's a secondary effect: video.ts books local spend the instant it observes status === "completed", without checking settlement succeeded. Safe before, because expiry was unreachable; in the 600–635s tail it would record a ledger charge for USDC that never moved.

Why not just lower the budget

Staying inside 600s unclamped requires budget <= 445s, which fails the 479s production job #110 exists to support. The budget number isn't the lever — the unbounded tail is.

Notes

pollTimeoutFor is extracted rather than inlined so the clamp is testable. Inline it sits inside the handler behind a 402 + payment + submit chain that test/video-models.test.ts blocks at the network by design (the NETWORK_ESCAPE sentinel).

The tool description's "9 min hard cap" becomes accurate once this lands, so it's unchanged.

Validation

  • npm test — 343 passed
  • npm run typecheck, npm run build — clean
  • Reverted the Math.min to a bare VIDEO_POLL_TIMEOUT_MS and confirmed the new test fails (not ok 15 - the last poll is clamped...), so it catches the regression it guards

…s authorization

The 540s budget was only 60s inside the 600s payment authorization at loop
ENTRY. The loop body then added a 5s interval sleep and an unbounded 90s poll
timeout, so a poll entered at 539.9s stayed in flight until 635s — 35s PAST
validBefore, not 60s before it.

Settlement happens server-side on the poll that answers "completed", so a poll
outliving the authorization fails settlement for a video that actually
generated. blockrun_music hit this exact failure (see CHANGELOG) and was fixed
by raising the window to 600s; this restores video's side of that invariant.

Lowering the budget instead would have defeated the PR: the production jobs it
exists to support ran 479s, and staying inside 600s unclamped needs budget
<= 445s. Clamping the last poll to the remaining budget keeps 540s available
and bounds worst-case wall time at exactly 540s.

- extract pollTimeoutFor(), so the clamp is unit-testable rather than buried
  behind a 402 + payment + submit chain the suite blocks at the network
- name VIDEO_POLL_TIMEOUT_MS and VIDEO_PAYMENT_AUTH_SECONDS, and use the latter
  where the authorization is signed
- replace the constants-only assertion (which restated 600 as a literal and
  ignored the interval and poll timeout entirely) with the real invariant plus
  a swept property test: a poll started at any reachable instant finishes on or
  before the deadline
@VickyXAI
VickyXAI merged commit bbfb8ff into main Aug 28, 2026
1 check passed
@VickyXAI
VickyXAI deleted the fix/video-poll-clamp branch August 28, 2026 04:43
VickyXAI added a commit that referenced this pull request Aug 28, 2026
…t the poll budget (#113)

blockrun_video got this in #111; music has the same shape and the same latent
bug, one budget bump away from firing.

music measures its 240s poll budget from AFTER submit, but validBefore is
counted from signing — and submit is allowed 95s. So the real worst case from
the authorization's own origin is 95 + 240 + 5 + 90 = 430s. That fits inside
600s today, which is exactly why it looks fine and why the next person to raise
MUSIC_POLL_BUDGET_MS the way #110 raised video's would walk straight into it:
at 420s the sum is 610s and settlement starts failing for tracks that rendered.

Naively copying video's fix (stamp startedAt before signing) would have been
worse than the bug: the 240s budget would then absorb up to 95s of submit,
leaving as little as 145s of polling for MiniMax tracks the code documents as
taking 60-180s. So the loop takes the EARLIER of two deadlines instead — the
poll budget, and the authorization minus settle-side slack. A fast submit keeps
the full 240s window; a slow one shortens it rather than silently pushing polls
past validBefore.

- share pollTimeoutFor from src/utils/poll.ts, used by both tools
- deliberately not in http.ts: four test files replace that module with a
  network sentinel, so a pure function there forces them to stub arithmetic
- video.ts: repair the indentation break at the spend-booking site and drop the
  redundant inner !spendBooked guard already implied by the enclosing condition
- cover the clamp in test/poll.test.ts and the music invariant in music-cost

Co-authored-by: VickyXAI <vicky.fuyu@gmail.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.

2 participants