Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); perf(core): load replay suffix in one request by NathanColosimo · Pull Request #3205 · vercel/workflow · GitHub
Skip to content

perf(core): load replay suffix in one request - #3205

Merged
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events
Aug 8, 2026
Merged

perf(core): load replay suffix in one request#3205
NathanColosimo merged 65 commits into
codex/stream-run-started-replay-pagefrom
codex/stream-all-events

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define an omitted event pagination.limit as every remaining event, while a numeric limit continues to request one bounded page
  • keep the full-load choice inside the existing pagination object instead of adding returnAll to the public World API
  • translate an omitted limit into the explicit all-events transport option at the Vercel boundary
  • preserve each validated Vercel event frame and resume a truncated full response after the last accepted event
  • bound Postgres raw-row memory by reading full results in 500-row internal query pages while returning one combined result
  • use full loading in wakeUpRun without changing the QuickJS runtime path
  • keep the configurable event budget as the Local and Postgres per-response ceiling and document the Vercel service-owned limit

Custom Worlds that still return bounded pages remain correct because the runtime continues following hasMore.

The base PR already receives the initial replay log from run_started. This PR removes repeated client requests for later replay loads after sleeps, hooks, and subsequent invocations.

Stack

Stacked on #3191.

Validation

  • all 41 monorepo typecheck tasks passed
  • world-vercel: 372 tests passed
  • core runtime helpers and run utilities: 94 focused tests passed
  • world-local storage: 244 tests passed
  • world-postgres storage: 133 tests passed, including a 501-event multi-query result
  • Biome formatting passed; checks report only existing repository complexity diagnostics

Docs Preview

PagePreview
Runtime tuningWORKFLOW_MAX_EVENTS

@changeset-bot

changeset-botBot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0613e5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercelvercelBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

Additional Suggestion:

Incremental stream polling skips newly-written chunks for streams larger than CHUNKS_PAGE_SIZE (500) because skipFrames is fed the absolute frame count from index 0 while the paired cursor points at a non-zero last-page start.

Fix on Vercel

This is not an issue in this PR, this pre existed on main, will fix in a separate PR

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment threadpackages/core/src/runtime/helpers.ts Outdated
Comment threadpackages/core/src/runtime/helpers.ts
Comment threadpackages/world/src/env-config.ts
Comment threadpackages/world/src/env-config.ts Outdated
Comment threadpackages/world-postgres/src/storage.ts Outdated
Comment threadpackages/world-local/src/storage.test.ts
Comment threadpackages/world/src/events.ts Outdated
Comment threaddocs/content/docs/v5/configuration/runtime-tuning.mdx

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling.
If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

LGTM but thinking about it more, I think returnAll might be a bit hacky. We could just bump the allowed max page size to 5k and use that by default in the call, no need for special handling. If we get above 5k, probably worth actually paginating? Or at least at that point it matters a little less for perf if we paginate

My POV is that the only point of paginating here is to reduce load on the world. but the runtime shouldn't be managing that, the world should

You are going to always require all events loaded into memory under the current architecture

The world should handle the load and all that. e.g. world-vercel streams down all events and paginates server side.
world-postgres paginates through postgres

but its all still 1 function call. The world could still do multiple http requests or whatever it wants to, but this lets certain worlds optimize by knowing ahead of time the caller wants all the events

@NathanColosimo

Copy link
Copy Markdown
ContributorAuthor

decided to explicitly keep returnAll on workflow-server to not change behavior on previous clients just in case

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 22349e9 (AI decision).

This is a performance optimization (perf(core)), not a fix for a user-visible defect: it collapses the replay-suffix load into a single World request by redefining an omitted pagination.limit as "return every remaining event". That is a semantic change to an existing public World API plus new surface (getMaxEventsPerRun exported from @workflow/world, returnAll=true on the Vercel transport, new WORKFLOW_MAX_EVENTS behavior for Local/Postgres events.list()), which is exactly the kind of behavior/feature change a maintenance branch should not take. The truncated-full-response resume logic in getWorkflowRunEventsV4 is resilience work, but it only exists to support the new full-load path introduced here, so there is no standalone fix to split out.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

22349e95fd85a112cbec3f425900b74bf5ccc77f

Sign up for freeto 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

@NathanColosimo@VaguelySerious