[stable] Harden event pagination responses#2179
Conversation
🦋 Changeset detectedLatest commit: e4da7eb The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (69 failed)mongodb-dev (1 failed):
redis-dev (1 failed):
turso-dev (1 failed):
turso (66 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
❌ 🌍 Community Worlds
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
There was a problem hiding this comment.
Pull request overview
Client-side defense in depth for cursor pagination in the stable runtime. Hardens getWorkflowRunEvents() against overlapping pages, rejected continuation cursors, and non-progressing pagination, and deduplicates events in the wait-completion replay append path.
Changes:
- Add dedup, cursor-rejection retry (once), and progress assertions to
getWorkflowRunEvents(). - Deduplicate
newEvents.eventsbefore appending in the wait-completion replay optimized path. - Add helper and replay tests plus a patch changeset.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/runtime/helpers.ts | Adds pagination dedup, contract-error assertions, and one-shot retry on 400 cursor rejection |
| packages/core/src/runtime.ts | Dedupes wait-completion delta events before appending |
| packages/core/src/runtime/helpers.test.ts | Tests for overlap dedup, 400 retry, cursor-repeat, and missing-cursor failures |
| packages/core/src/runtime/wait-completion-replay.test.ts | Test for delta that restarts at the beginning |
| .changeset/calm-events-guard.md | Patch changeset for @workflow/core |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
VaguelySerious
left a comment
There was a problem hiding this comment.
AI review: no blocking issues
| ); | ||
| } | ||
| if (requestedCursors.has(cursor)) { | ||
| throw eventPaginationContractError(runId, 'repeated a cursor'); |
There was a problem hiding this comment.
AI Review: Nit
The repeated-cursor guard terminates the common failure mode this PR targets (a backend that restarts the read at the beginning re-issues cursors we've already requested). It does not bound the loop against a backend that returns an endless stream of fresh cursors with hasMore: true — appendUniqueEvents keeps the result flat, but the while (hasMore) loop never exits. Since pagesLoaded is already tracked, a sanity cap (throwing the same contract error past, say, N pages) would fully close the non-progression hole rather than just the repeated-cursor case.
| return ( | ||
| cursor !== null && | ||
| !alreadyRetried && | ||
| WorkflowWorldError.is(error) && |
There was a problem hiding this comment.
AI Review: Note
The full-reload fallback only engages on error.status === 400. If a backend ever signals a rejected/expired cursor with a different 4xx (404/410/422), the retry won't trigger and the error propagates straight to run_failed. If the contract guarantees 400 for cursor rejection this is fine; otherwise consider matching the set of statuses the backend can use. (Also worth noting: a 400 surfaced on the cursorless initial load classifies as USER_ERROR via classifyRunError, since isWorldContractError bails when status is set — pre-existing, just flagging the interaction.)
Summary
Why
This is client-side defense in depth for the cursor compatibility issue addressed by vercel/workflow-server#454. On
stable, the optimized wait-completion replay path appends the cursor delta directly. If a server ignores the cursor and restarts the read at the beginning, previously committed events can be appended again and corrupt replay.The server still needs to preserve legacy cursor compatibility for already deployed clients. This backport also makes the stable client tolerant of overlapping or rejected cursor reads and fails cleanly for non-progressing pagination.
Validation
pnpm --filter '@workflow/core...' buildv24.15.0:pnpm --filter @workflow/core typecheckv24.15.0:pnpm --filter @workflow/core test(639tests passed)pnpm exec biome check packages/core/src/runtime/helpers.ts packages/core/src/runtime/helpers.test.ts packages/core/src/runtime/wait-completion-replay.test.ts .changeset/calm-events-guard.mdgit diff --check