Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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" + '
Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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('^' + ".*" + ' Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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('^' + ".*" + ' Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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" + ' Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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('^' + ".*" + ' Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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('^' + ".*" + ' Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@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); } })(); })(); Add world.snapshots storage interface (local, postgres, vercel) by TooTallNate · Pull Request #3050 · vercel/workflow · GitHub
Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3050

Merged
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots
Jul 31, 2026
Merged

Add world.snapshots storage interface (local, postgres, vercel)#3050
TooTallNate merged 0 commit into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

@changeset-bot

changeset-botBot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f43ee63

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

This PR includes changesets to release 20 packages
NameType
@workflow/worldMinor
@workflow/world-localMinor
@workflow/world-postgresMinor
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
workflowPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 1:12am
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 1:12am
example-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-express-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 1:12am
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 1:12am
workflow-docsReadyReadyPreview, v0Jul 31, 2026 1:12am
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 1:12am
workflow-tarballsReadyReadyPreviewJul 31, 2026 1:12am
workflow-webReadyReadyPreviewJul 31, 2026 1:12am

@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (2 failed)

nextjs-turbopack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYTVK8V80GX7SNQ7TW7Y8XCN

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development279904353234
❌ 📦 Local Production324024543696
✅ 🐘 Local Postgres324204543696
✅ 🪟 Windows30800308
✅ 📋 Other178803682156
✅ vercel-multi-region270027
Total128592195014811
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node126028
✅ example-node126028
✅ express-node126028
✅ fastify-node126028
✅ hono-node126028
✅ nextjs-turbopack-node15103
✅ nextjs-webpack-node15103
✅ nitro-node126028
✅ nuxt-node126028
✅ sveltekit-node14509
✅ vite-node126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
❌ nextjs-turbopack-stable-node15310
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
❌ nextjs-webpack-stable-node15310
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node128026
✅ astro-stable-quickjs128026
✅ express-stable-node128026
✅ express-stable-quickjs128026
✅ fastify-stable-node128026
✅ fastify-stable-quickjs128026
✅ hono-stable-node128026
✅ hono-stable-quickjs128026
✅ nextjs-turbopack-canary-node135019
✅ nextjs-turbopack-canary-quickjs135019
✅ nextjs-turbopack-stable-node15400
✅ nextjs-turbopack-stable-quickjs15400
✅ nextjs-webpack-canary-node135019
✅ nextjs-webpack-canary-quickjs135019
✅ nextjs-webpack-stable-node15400
✅ nextjs-webpack-stable-quickjs15400
✅ nitro-stable-node128026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node128026
✅ nuxt-stable-quickjs128026
✅ sveltekit-stable-node14707
✅ sveltekit-stable-quickjs14707
✅ vite-stable-node128026
✅ vite-stable-quickjs128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15400
✅ nextjs-turbopack-quickjs15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node128026
✅ e2e-local-dev-nest-stable-quickjs128026
✅ e2e-local-dev-tanstack-start-node128026
✅ e2e-local-dev-tanstack-start-quickjs128026
✅ e2e-local-postgres-nest-stable-node128026
✅ e2e-local-postgres-nest-stable-quickjs128026
✅ e2e-local-postgres-tanstack-start-node128026
✅ e2e-local-postgres-tanstack-start-quickjs128026
✅ e2e-local-prod-nest-stable-node128026
✅ e2e-local-prod-nest-stable-quickjs128026
✅ e2e-local-prod-tanstack-start-node128026
✅ e2e-local-prod-tanstack-start-quickjs128026
✅ e2e-vercel-prod-nest-node126028
✅ e2e-vercel-prod-tanstack-start-node126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

*/
export function createSnapshotsStorage(basedir: string) {
const snapshotsDir = path.join(basedir, 'snapshots');

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.

snapshots-storage.ts builds filesystem paths from an untrusted runId without validation, allowing path traversal outside the {basedir}/snapshots/ directory in save/load/delete.

Fix on Vercel

@TooTallNate
TooTallNate merged commit f64932e into quickjs-vm-perfJul 31, 2026
@TooTallNate
TooTallNate deleted the quickjs-vm-snapshots branch July 31, 2026 02:15
@TooTallNate
TooTallNateforce-pushed the quickjs-vm-snapshots branch from f43ee63 to f64932eCompareJuly 31, 2026 02:15
@VaguelySerious

VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

AI review: blocking issues found

Note on mechanics: this PR's diff renders empty (f64932e5 is also #3049's head) and it has no CI checks, so there is nothing to anchor inline comments to. Findings below are against the snapshots storage interface as it exists on the branch and as consumed by #3251.

AI Review: Blocking

snapshots is added as a required member of Storage on a minor changeset. Any third-party world implementation — the interface is public and community adapters exist — fails to typecheck on upgrade. Either make it optional with the consumer treating absence as "snapshotting unavailable", or ship it as a major. #3251 already tolerates every runtime failure of this interface by falling back to full replay, so an optional member costs it nothing.

AI Review: Note

No integrity binding between data and metadata. The payload and the eventsCursor that describes it are stored as two independent values, and the local world writes them as a separate .bin and .json pair with no atomicity. A crash between the two writes leaves a heap paired with a stale cursor, and the restore path in #3251 cannot detect it — it replays the wrong delta against a heap that already consumed those events. Storing the metadata inside the encrypted payload (or a checksum of the payload in the metadata) makes the mismatch detectable rather than silent.

Postgres table has no foreign key, cascade, or GC. Rows are only removed by an explicit delete call, which #3251 issues from the completed and failed branches only — cancelled and abandoned runs leak their snapshot permanently. A cascade on the run row, or a createdAt-based TTL sweep, makes cleanup independent of which client paths remember to call delete.

console.debug on every save and load. These fire per snapshot operation on the hot path and bypass the package's logger, so they cannot be filtered or leveled by operators. runtimeLogger.debug is already imported nearby.

load and delete bypass the retry/metrics helper that save uses. So a transient failure on the load path is a snapshot miss with no retry and no metric, and the resulting silent fallback to full replay looks identical to "no snapshot existed". Given the whole feature is invisible when it fails, the read path needs the instrumentation at least as much as the write path.

AI Review: Nit

Comments and the changeset name the internal backend service. In this repository that should be world-vercel.

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

@TooTallNate@VaguelySerious