Skip to content

[world-local] Reduce sequential replay I/O#2152

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/world-local-sequential-perf
Jun 30, 2026
Merged

[world-local] Reduce sequential replay I/O#2152
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/world-local-sequential-perf

Conversation

@pranaygp

@pranaygp pranaygp commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • cache a bounded recent window of append-only local events so immediate replay pagination avoids rereading JSON files
  • cache storage directories created by this process so sequential event writes avoid repeated recursive mkdir syscalls
  • preserve correctness under relative data directories, mutation, cleanup, and long-lived active runs with focused regression coverage and a patch changeset

Root cause

The existing sequentialStepsWorkflow(count, 0) benchmark reproduces the zero-work sequential-step shape. On the PR merge base, its local-world storage work is dominated by three persisted lifecycle events per step and the incremental events.list() call used for replay. The listing path rereads append-only event files that the same storage instance just wrote, while the write path repeatedly calls mkdir(..., { recursive: true }) for fixed directories.

This workload does not exercise streams; the previously landed stream metadata optimization is separate from this path.

Correctness and memory safeguards

Reviewing the caching implementation exposed three correctness issues that are covered here:

  • cached event lookup now resolves relative data directories before using the absolute-path cache key, so the ordinary local-world configuration receives the optimization
  • cached event entries are decoded from the serialized snapshot through EventSchema, so they are detached from caller mutations and have the same normalized shape as disk reads
  • atomic and exclusive writes retry once after recreating a cached directory removed externally while a dev server is still running

Retention is explicitly bounded:

  • the recent-event cache is capped at 4 MiB and 1000 entries across active runs; oversized events are read from disk instead of retained
  • cached events are released for terminal runs and when world.clear() or world.close() is called
  • tests exercise eviction after exceeding the byte limit and correlation-id cache queries

Measurement

I modeled the event/replay lifecycle for a no-delay sequential workflow directly through @workflow/world-local storage with a relative dataDir, matching normal local-world configuration. Both revisions ran the same probe with one warmup and five measured trials per size; medians are reported. The control is the PR merge base (ae37315cb).

Sequential steps Merge base This branch Improvement
50 178.49 ms 124.97 ms 30.0% faster
200 742.24 ms 609.04 ms 17.9% faster

For 200 steps, incremental events.list() time fell from 193.35 ms to 104.86 ms (45.8% lower).

A 50-step filesystem-operation trace demonstrates the removed work:

Operation Merge-base calls This branch calls
readFile 457 252
mkdir 404 4

An end-to-end workbench probe also showed that most remaining no-delay sequential-workflow latency occurs above this storage path: a 200-step run reported 22.8 s inside /.well-known/workflow/v1/flow.

Note: the figures above were captured against the original merge base ae37315cb, before the merge with main below. They characterize the optimization relative to its baseline and have not been re-measured against current main HEAD.

Merge with main

This branch has been merged up to the latest main. main reworked the local event-write path in the interim (atomic publish via writeExclusive temp-file + hard-link, per-instance stepLocks/hookLocks, and hook_created dedup-recovery). Conflicts were resolved so the recent-event cache layers on top of those write semantics rather than replacing them:

  • writeExclusive keeps main's atomic temp-file + hard-link publish (and Windows retry), now wrapped in the branch's withEnsuredDirectory ENOENT-retry for externally-removed data dirs
  • event caching moved into a rememberStoredEvent() helper invoked after each successful publish (both the main event path and the hook_conflict path), preserving the pre-await serialized-snapshot detachment guarantee

Validation

  • reproduced and fixed relative-dataDir cache misses; a five-step sequential lifecycle regression test now verifies repeated event lists make zero event-file reads
  • reproduced the pre-fix cached-object alias and verified the amended implementation returns the persisted value
  • reproduced the pre-fix external-directory cleanup failure (ENOENT on the next event write) and verified directory recovery
  • added cache-hit coverage for listByCorrelationId() and FIFO byte-limit eviction without a Windows-hostile high-file-count test
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-local test (427 tests passed, post-merge)
  • pnpm --filter '@workflow/world-local...' build
  • pnpm changeset status --since=origin-https/main
  • git diff --check

@changeset-bot

changeset-bot Bot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0a0264f

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

This PR includes changesets to release 18 packages
Name Type
@workflow/world-local Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/vitest Patch
@workflow/world-postgres Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

vercel Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jun 29, 2026 9:15pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jun 29, 2026 9:15pm
example-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-astro-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-express-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-fastify-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-hono-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-nitro-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workbench-vite-workflow Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jun 29, 2026 9:15pm
workflow-swc-playground Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workflow-tarballs Ready Ready Preview, Comment Jun 29, 2026 9:15pm
workflow-web Ready Ready Preview, Comment Jun 29, 2026 9:15pm

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1441 1 230 1672
✅ 💻 Local Development 1605 0 219 1824
✅ 📦 Local Production 1605 0 219 1824
✅ 🐘 Local Postgres 1593 0 231 1824
✅ 🪟 Windows 152 0 0 152
✅ 📋 Other 885 0 179 1064
Total 7281 1 1078 8360

❌ Failed Tests

▲ Vercel Production (1 failed)

fastify (1 failed):

  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_01KWAM2C6SS2EHJ226Z88KA764 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 125 0 27
✅ example 125 0 27
✅ express 125 0 27
❌ fastify 124 1 27
✅ hono 125 0 27
✅ nextjs-turbopack 149 0 3
✅ nextjs-webpack 149 0 3
✅ nitro 125 0 27
✅ nuxt 125 0 27
✅ sveltekit 144 0 8
✅ vite 125 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 0 26
✅ express-stable 126 0 26
✅ fastify-stable 126 0 26
✅ hono-stable 126 0 26
✅ nextjs-turbopack-canary 132 0 20
✅ nextjs-turbopack-stable 151 0 1
✅ nextjs-webpack-canary 132 0 20
✅ nextjs-webpack-stable 151 0 1
✅ nitro-stable 126 0 26
✅ nuxt-stable 126 0 26
✅ sveltekit-stable 145 0 7
✅ vite-stable 126 0 26
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 152 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 127 0 25
✅ e2e-local-dev-tanstack-start- 127 0 25
✅ e2e-local-postgres-nest-stable 126 0 26
✅ e2e-local-postgres-tanstack-start- 126 0 26
✅ e2e-local-prod-nest-stable 127 0 25
✅ e2e-local-prod-tanstack-start- 127 0 25
✅ e2e-vercel-prod-tanstack-start 125 0 27

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.047s (-28.6% 🟢) 1.011s (~) 0.964s 10 1.00x
💻 Local Nitro 0.047s (+7.7% 🔺) 1.007s (~) 0.960s 10 1.01x
💻 Local Next.js (Turbopack) 0.048s (-15.0% 🟢) 1.006s (~) 0.958s 10 1.01x
💻 Local Express 0.048s (~) 1.007s (~) 0.959s 10 1.03x
🐘 Postgres Next.js (Turbopack) 0.060s (-1.0%) 1.014s (~) 0.953s 10 1.28x
🐘 Postgres Express 0.072s (+13.0% 🔺) 1.012s (~) 0.940s 10 1.54x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.220s (+4.3%) 1.906s (-5.6% 🟢) 1.686s 10 1.00x
▲ Vercel Express 0.301s (+16.2% 🔺) 2.187s (+4.1%) 1.886s 10 1.37x
▲ Vercel Next.js (Turbopack) 0.742s (+202.2% 🔺) 2.602s (+9.6% 🔺) 1.861s 10 3.38x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.083s (-1.0%) 2.006s (~) 0.923s 10 1.00x
💻 Local Nitro 1.084s (~) 2.007s (~) 0.923s 10 1.00x
💻 Local Express 1.085s (~) 2.006s (~) 0.922s 10 1.00x
🐘 Postgres Nitro 1.092s (~) 2.018s (~) 0.925s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.104s (+0.9%) 2.010s (~) 0.906s 10 1.02x
🐘 Postgres Express 1.104s (+1.2%) 2.010s (~) 0.906s 10 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.367s (-1.2%) 2.688s (-6.4% 🟢) 1.321s 10 1.00x
▲ Vercel Express 1.457s (+4.1%) 3.192s (+3.8%) 1.735s 10 1.07x
▲ Vercel Next.js (Turbopack) 2.368s (+0.5%) 4.063s (+10.3% 🔺) 1.695s 10 1.73x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.307s (-1.7%) 11.016s (~) 0.709s 3 1.00x
💻 Local Express 10.454s (~) 11.023s (~) 0.569s 3 1.01x
🐘 Postgres Express 10.470s (~) 11.012s (~) 0.542s 3 1.02x
💻 Local Nitro 10.471s (~) 11.023s (~) 0.552s 3 1.02x
💻 Local Next.js (Turbopack) 10.484s (~) 11.021s (~) 0.538s 3 1.02x
🐘 Postgres Next.js (Turbopack) 10.558s (~) 11.022s (~) 0.464s 3 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 12.089s (+3.6%) 14.019s (+5.7% 🔺) 1.930s 3 1.00x
▲ Vercel Nitro 12.266s (+4.6%) 13.894s (+1.5%) 1.629s 3 1.01x
▲ Vercel Next.js (Turbopack) 12.655s (-3.8%) 14.022s (-5.7% 🟢) 1.367s 3 1.05x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 13.292s (-2.1%) 14.018s (~) 0.725s 5 1.00x
💻 Local Next.js (Turbopack) 13.559s (-1.3%) 14.026s (~) 0.467s 5 1.02x
🐘 Postgres Express 13.614s (~) 14.019s (~) 0.405s 5 1.02x
💻 Local Express 13.651s (~) 14.027s (~) 0.376s 5 1.03x
💻 Local Nitro 13.680s (+0.7%) 14.030s (~) 0.349s 5 1.03x
🐘 Postgres Next.js (Turbopack) 13.704s (+0.6%) 14.023s (~) 0.319s 5 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 16.596s (+2.0%) 18.301s (-1.8%) 1.705s 4 1.00x
▲ Vercel Express 16.768s (+2.1%) 18.892s (+4.7%) 2.124s 4 1.01x
▲ Vercel Next.js (Turbopack) 17.950s (-4.2%) 19.419s (-5.5% 🟢) 1.469s 4 1.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 11.372s (-7.0% 🟢) 12.013s (-7.7% 🟢) 0.640s 8 1.00x
💻 Local Next.js (Turbopack) 12.180s (-2.0%) 13.027s (~) 0.847s 7 1.07x
💻 Local Express 12.210s (~) 13.026s (~) 0.817s 7 1.07x
💻 Local Nitro 12.271s (~) 13.027s (~) 0.756s 7 1.08x
🐘 Postgres Express 12.297s (+1.0%) 13.018s (~) 0.721s 7 1.08x
🐘 Postgres Next.js (Turbopack) 12.315s (-2.2%) 13.015s (-1.1%) 0.700s 7 1.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 19.810s (+12.0% 🔺) 21.683s (+13.1% 🔺) 1.873s 5 1.00x
▲ Vercel Nitro 20.970s (+17.1% 🔺) 22.334s (+13.1% 🔺) 1.364s 5 1.06x
▲ Vercel Next.js (Turbopack) 21.732s (+6.5% 🔺) 23.672s (+4.9%) 1.940s 4 1.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.183s (~) 2.007s (~) 0.824s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.189s (-0.9%) 2.008s (~) 0.819s 15 1.01x
🐘 Postgres Nitro 1.214s (+0.5%) 2.006s (~) 0.793s 15 1.03x
💻 Local Next.js (Turbopack) 1.381s (-9.2% 🟢) 2.006s (~) 0.625s 15 1.17x
💻 Local Nitro 1.408s (+2.1%) 2.007s (~) 0.599s 15 1.19x
💻 Local Express 1.441s (+0.7%) 2.007s (~) 0.566s 15 1.22x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.359s (+3.4%) 4.210s (+9.4% 🔺) 1.850s 8 1.00x
▲ Vercel Express 2.449s (+11.3% 🔺) 4.077s (+16.6% 🔺) 1.628s 8 1.04x
▲ Vercel Next.js (Turbopack) 4.180s (+31.1% 🔺) 6.023s (+22.9% 🔺) 1.843s 5 1.77x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.331s (-2.3%) 2.592s (+11.9% 🔺) 1.260s 12 1.00x
🐘 Postgres Next.js (Turbopack) 1.333s (+1.1%) 3.109s (+6.5% 🔺) 1.776s 10 1.00x
🐘 Postgres Nitro 1.558s (+17.2% 🔺) 2.625s (+9.7% 🔺) 1.067s 12 1.17x
💻 Local Next.js (Turbopack) 2.136s (-16.8% 🟢) 2.826s (-9.1% 🟢) 0.690s 11 1.60x
💻 Local Nitro 2.387s (-4.1%) 3.010s (+3.2%) 0.623s 10 1.79x
💻 Local Express 2.397s (-12.5% 🟢) 3.009s (-6.3% 🟢) 0.612s 10 1.80x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.634s (+7.6% 🔺) 4.366s (+22.4% 🔺) 1.732s 7 1.00x
▲ Vercel Nitro 3.921s (+55.7% 🔺) 5.340s (+39.6% 🔺) 1.420s 6 1.49x
▲ Vercel Next.js (Turbopack) 4.358s (+21.8% 🔺) 5.901s (+11.1% 🔺) 1.543s 6 1.65x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.439s (-10.3% 🟢) 3.208s (-22.5% 🟢) 1.769s 10 1.00x
🐘 Postgres Express 1.617s (+0.8%) 4.135s (~) 2.519s 8 1.12x
🐘 Postgres Next.js (Turbopack) 2.659s (-10.2% 🟢) 5.684s (-5.5% 🟢) 3.025s 6 1.85x
💻 Local Nitro 4.490s (-21.7% 🟢) 5.157s (-22.1% 🟢) 0.667s 7 3.12x
💻 Local Next.js (Turbopack) 4.593s (-32.4% 🟢) 5.011s (-33.3% 🟢) 0.418s 6 3.19x
💻 Local Express 4.749s (-33.2% 🟢) 5.347s (-29.8% 🟢) 0.598s 6 3.30x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.933s (+46.7% 🔺) 5.766s (+27.7% 🔺) 1.833s 6 1.00x
▲ Vercel Nitro 4.339s (+61.0% 🔺) 5.701s (+26.1% 🔺) 1.361s 6 1.10x
▲ Vercel Next.js (Turbopack) 5.709s (+40.6% 🔺) 7.363s (+23.4% 🔺) 1.655s 5 1.45x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.131s (-5.3% 🟢) 2.008s (~) 0.877s 15 1.00x
🐘 Postgres Express 1.184s (-0.7%) 2.007s (~) 0.823s 15 1.05x
🐘 Postgres Next.js (Turbopack) 1.201s (~) 2.008s (~) 0.807s 15 1.06x
💻 Local Next.js (Turbopack) 1.400s (-5.6% 🟢) 2.006s (~) 0.607s 15 1.24x
💻 Local Express 1.413s (-1.7%) 2.007s (~) 0.594s 15 1.25x
💻 Local Nitro 1.461s (+8.3% 🔺) 2.007s (~) 0.546s 15 1.29x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.718s (+30.6% 🔺) 4.449s (+33.4% 🔺) 1.731s 7 1.00x
▲ Vercel Nitro 2.871s (+46.3% 🔺) 4.453s (+19.0% 🔺) 1.581s 7 1.06x
▲ Vercel Next.js (Turbopack) 3.885s (+26.3% 🔺) 5.514s (+15.7% 🔺) 1.629s 6 1.43x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.237s (-5.2% 🟢) 2.006s (-20.0% 🟢) 0.769s 15 1.00x
🐘 Postgres Express 1.322s (~) 2.507s (+4.7%) 1.185s 12 1.07x
🐘 Postgres Next.js (Turbopack) 1.402s (+6.6% 🔺) 3.010s (~) 1.608s 10 1.13x
💻 Local Next.js (Turbopack) 2.437s (-9.4% 🟢) 3.008s (-10.0% 🟢) 0.571s 10 1.97x
💻 Local Nitro 2.510s (+1.5%) 3.009s (~) 0.499s 10 2.03x
💻 Local Express 2.550s (+0.6%) 3.009s (-6.3% 🟢) 0.459s 10 2.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.700s (+12.3% 🔺) 4.572s (+25.3% 🔺) 1.872s 7 1.00x
▲ Vercel Nitro 2.881s (+8.2% 🔺) 4.423s (+6.1% 🔺) 1.542s 7 1.07x
▲ Vercel Next.js (Turbopack) 3.955s (+17.9% 🔺) 5.521s (+10.0% 🔺) 1.566s 6 1.46x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.462s (-9.0% 🟢) 3.209s (-22.4% 🟢) 1.747s 10 1.00x
🐘 Postgres Express 1.616s (-0.8%) 4.136s (~) 2.521s 8 1.11x
🐘 Postgres Next.js (Turbopack) 3.006s (+17.1% 🔺) 6.218s (+3.3%) 3.212s 5 2.06x
💻 Local Express 5.398s (-22.5% 🟢) 6.013s (-22.6% 🟢) 0.615s 5 3.69x
💻 Local Nitro 5.540s (-8.9% 🟢) 6.216s (-8.8% 🟢) 0.676s 5 3.79x
💻 Local Next.js (Turbopack) 5.568s (-22.7% 🟢) 6.015s (-22.6% 🟢) 0.448s 6 3.81x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.426s (+13.0% 🔺) 5.237s (+11.9% 🔺) 1.812s 6 1.00x
▲ Vercel Nitro 4.057s (+35.2% 🔺) 5.515s (+12.1% 🔺) 1.458s 6 1.18x
▲ Vercel Next.js (Turbopack) 5.030s (+12.9% 🔺) 6.347s (+3.3%) 1.317s 5 1.47x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.381s (-29.5% 🟢) 1.006s (-1.6%) 0.626s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.572s (+3.8%) 1.007s (~) 0.435s 60 1.50x
🐘 Postgres Express 0.574s (+4.8%) 1.041s (+1.7%) 0.466s 58 1.51x
💻 Local Next.js (Turbopack) 0.599s (-3.9%) 1.005s (~) 0.406s 60 1.57x
💻 Local Express 0.601s (+1.6%) 1.005s (~) 0.404s 60 1.58x
💻 Local Nitro 0.636s (+7.9% 🔺) 1.005s (-1.6%) 0.370s 60 1.67x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.016s (+20.1% 🔺) 4.926s (+22.8% 🔺) 1.910s 13 1.00x
▲ Vercel Nitro 3.055s (+14.7% 🔺) 4.620s (+7.9% 🔺) 1.565s 13 1.01x
▲ Vercel Next.js (Turbopack) 4.222s (+8.2% 🔺) 6.142s (+9.5% 🔺) 1.921s 11 1.40x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.977s (-24.0% 🟢) 1.312s (-34.6% 🟢) 0.335s 70 1.00x
🐘 Postgres Express 1.336s (+3.1%) 2.029s (+1.1%) 0.693s 45 1.37x
🐘 Postgres Next.js (Turbopack) 1.375s (+5.1% 🔺) 2.008s (~) 0.633s 45 1.41x
💻 Local Next.js (Turbopack) 1.476s (-4.1%) 2.006s (~) 0.529s 45 1.51x
💻 Local Express 1.512s (+2.7%) 2.006s (~) 0.495s 45 1.55x
💻 Local Nitro 1.538s (+8.0% 🔺) 2.007s (~) 0.469s 45 1.57x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 7.970s (+32.3% 🔺) 9.654s (+26.2% 🔺) 1.685s 10 1.00x
▲ Vercel Express 8.444s (+41.5% 🔺) 10.453s (+44.1% 🔺) 2.009s 9 1.06x
▲ Vercel Next.js (Turbopack) 9.911s (+19.9% 🔺) 11.632s (+19.2% 🔺) 1.721s 8 1.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.935s (-23.8% 🟢) 2.356s (-23.0% 🟢) 0.421s 52 1.00x
🐘 Postgres Express 2.615s (+1.5%) 3.058s (-0.9%) 0.444s 40 1.35x
🐘 Postgres Next.js (Turbopack) 2.804s (+3.6%) 3.112s (+0.8%) 0.308s 39 1.45x
💻 Local Express 3.242s (+0.9%) 4.009s (~) 0.767s 30 1.68x
💻 Local Next.js (Turbopack) 3.245s (-4.7%) 3.977s (-1.6%) 0.732s 31 1.68x
💻 Local Nitro 3.354s (+5.7% 🔺) 4.010s (+5.8% 🔺) 0.656s 30 1.73x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 14.987s (+24.3% 🔺) 16.561s (+20.5% 🔺) 1.574s 8 1.00x
▲ Vercel Express 15.440s (+32.3% 🔺) 17.571s (+33.0% 🔺) 2.131s 7 1.03x
▲ Vercel Next.js (Turbopack) 20.372s (+24.7% 🔺) 21.652s (+18.5% 🔺) 1.280s 6 1.36x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.160s (-25.6% 🟢) 1.005s (~) 0.846s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.195s (+10.7% 🔺) 1.007s (~) 0.812s 60 1.22x
🐘 Postgres Express 0.211s (-1.5%) 1.006s (~) 0.795s 60 1.32x
💻 Local Express 0.543s (+23.9% 🔺) 1.022s (+1.7%) 0.479s 59 3.40x
💻 Local Nitro 0.560s (+25.9% 🔺) 1.022s (+1.8%) 0.463s 59 3.50x
💻 Local Next.js (Turbopack) 0.618s (-5.8% 🟢) 1.021s (~) 0.403s 59 3.87x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.267s (+23.5% 🔺) 2.940s (+25.4% 🔺) 1.673s 21 1.00x
▲ Vercel Nitro 1.275s (+22.8% 🔺) 2.561s (-2.4%) 1.286s 24 1.01x
▲ Vercel Next.js (Turbopack) 2.526s (+21.0% 🔺) 3.927s (+4.0%) 1.401s 16 1.99x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.282s (-12.8% 🟢) 1.030s (+2.4%) 0.749s 88 1.00x
🐘 Postgres Next.js (Turbopack) 0.309s (+9.5% 🔺) 1.053s (+3.5%) 0.744s 86 1.10x
🐘 Postgres Express 0.335s (~) 1.006s (~) 0.671s 90 1.19x
💻 Local Express 2.457s (+16.7% 🔺) 3.010s (+10.0% 🔺) 0.553s 30 8.73x
💻 Local Next.js (Turbopack) 2.544s (-9.4% 🟢) 3.008s (-15.2% 🟢) 0.464s 30 9.04x
💻 Local Nitro 2.614s (+19.5% 🔺) 3.076s (+10.0% 🔺) 0.463s 30 9.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.116s (+60.3% 🔺) 4.169s (+62.9% 🔺) 2.053s 23 1.00x
▲ Vercel Nitro 2.374s (+78.5% 🔺) 3.965s (+41.3% 🔺) 1.591s 23 1.12x
▲ Vercel Next.js (Turbopack) 3.622s (+38.2% 🔺) 5.306s (+31.2% 🔺) 1.684s 18 1.71x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.426s (-18.0% 🟢) 1.027s (~) 0.600s 117 1.00x
🐘 Postgres Next.js (Turbopack) 0.532s (+1.0%) 3.087s (-0.8%) 2.555s 39 1.25x
🐘 Postgres Express 0.539s (+1.6%) 1.059s (+0.9%) 0.520s 114 1.26x
💻 Local Nitro 4.917s (-49.1% 🟢) 8.556s (-20.6% 🟢) 3.639s 15 11.54x
💻 Local Express 5.785s (-42.5% 🟢) 8.667s (-22.7% 🟢) 2.882s 14 13.57x
💻 Local Next.js (Turbopack) 5.995s (-41.1% 🟢) 8.670s (-22.7% 🟢) 2.675s 14 14.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.448s (+20.6% 🔺) 4.030s (+2.4%) 1.582s 30 1.00x
▲ Vercel Express 2.549s (+44.8% 🔺) 4.506s (+31.1% 🔺) 1.958s 27 1.04x
▲ Vercel Next.js (Turbopack) 5.371s (+36.1% 🔺) 7.263s (+29.7% 🔺) 1.892s 17 2.19x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.108s (-5.3% 🟢) 1.996s (~) 0.001s (-58.3% 🟢) 2.008s (~) 0.900s 10 1.00x
💻 Local Next.js (Turbopack) 1.134s (-2.7%) 1.970s (+0.6%) 0.012s (-4.7%) 2.020s (~) 0.886s 10 1.02x
💻 Local Express 1.160s (~) 2.005s (~) 0.012s (-1.6%) 2.020s (~) 0.860s 10 1.05x
🐘 Postgres Express 1.163s (~) 1.997s (~) 0.001s (~) 2.011s (~) 0.848s 10 1.05x
🐘 Postgres Next.js (Turbopack) 1.163s (~) 2.000s (~) 0.001s (~) 2.011s (~) 0.848s 10 1.05x
💻 Local Nitro 1.164s (~) 2.004s (~) 0.013s (+20.0% 🔺) 2.020s (~) 0.855s 10 1.05x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.999s (~) 3.472s (+17.8% 🔺) 2.271s (+9.7% 🔺) 6.267s (+13.7% 🔺) 4.268s 10 1.00x
▲ Vercel Nitro 2.151s (+6.3% 🔺) 3.230s (-1.5%) 1.892s (+11.3% 🔺) 5.603s (+1.7%) 3.452s 10 1.08x
▲ Vercel Next.js (Turbopack) 3.886s (+8.3% 🔺) 3.868s (-1.9%) 1.724s (+20.4% 🔺) 6.961s (+0.6%) 3.075s 10 1.94x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.569s (+0.8%) 2.010s (~) 0.013s (+3.4%) 2.027s (~) 0.457s 30 1.00x
🐘 Postgres Express 1.570s (~) 2.003s (~) 0.005s (+2.8%) 2.027s (~) 0.456s 30 1.00x
💻 Local Next.js (Turbopack) 1.578s (-4.6%) 1.971s (-1.5%) 0.010s (-22.5% 🟢) 2.023s (-1.8%) 0.445s 30 1.01x
🐘 Postgres Nitro 1.602s (+4.0%) 2.102s (+4.9%) 0.003s (-38.5% 🟢) 2.127s (+5.1% 🔺) 0.525s 29 1.02x
💻 Local Nitro 1.604s (+3.8%) 2.009s (~) 0.014s (+15.3% 🔺) 2.028s (~) 0.423s 30 1.02x
🐘 Postgres Next.js (Turbopack) 1.634s (-1.1%) 2.011s (~) 0.005s (-8.9% 🟢) 2.027s (~) 0.392s 30 1.04x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.367s (+14.3% 🔺) 8.277s (+20.8% 🔺) 0.195s (-9.9% 🟢) 9.109s (+21.6% 🔺) 2.742s 7 1.00x
▲ Vercel Nitro 6.689s (+18.6% 🔺) 7.771s (+9.2% 🔺) 0.418s (+79.9% 🔺) 8.645s (+10.3% 🔺) 1.957s 7 1.05x
▲ Vercel Next.js (Turbopack) 10.375s (+12.6% 🔺) 11.316s (+9.2% 🔺) 0.257s (-51.6% 🟢) 12.402s (+4.3%) 2.027s 5 1.63x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.765s (~) 1.026s (-1.9%) 0.000s (-1.7%) 1.051s (-0.8%) 0.286s 58 1.00x
🐘 Postgres Nitro 0.942s (+22.6% 🔺) 1.276s (+21.9% 🔺) 0.000s (+143.5% 🔺) 1.309s (+21.7% 🔺) 0.367s 46 1.23x
🐘 Postgres Next.js (Turbopack) 1.077s (+3.3%) 1.579s (+2.7%) 0.000s (+Infinity% 🔺) 1.599s (+3.4%) 0.522s 38 1.41x
💻 Local Next.js (Turbopack) 1.260s (-10.7% 🟢) 1.978s (~) 0.001s (-42.3% 🟢) 2.016s (~) 0.756s 30 1.65x
💻 Local Express 1.411s (+9.1% 🔺) 1.920s (-4.6%) 0.000s (+181.3% 🔺) 1.923s (-4.6%) 0.512s 32 1.84x
💻 Local Nitro 1.430s (+10.4% 🔺) 1.981s (-1.6%) 0.000s (-3.2%) 1.984s (-1.6%) 0.554s 31 1.87x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.295s (+10.7% 🔺) 4.389s (+0.6%) 0.000s (-100.0% 🟢) 4.845s (~) 1.550s 13 1.00x
▲ Vercel Express 3.703s (+28.2% 🔺) 5.043s (+21.0% 🔺) 0.000s (-100.0% 🟢) 5.627s (+22.1% 🔺) 1.924s 11 1.12x
▲ Vercel Next.js (Turbopack) 5.261s (+17.4% 🔺) 5.880s (+8.0% 🔺) 0.000s (+Infinity% 🔺) 6.815s (+8.5% 🔺) 1.555s 9 1.60x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.822s (+3.8%) 2.344s (+2.3%) 0.000s (-48.1% 🟢) 2.359s (+2.4%) 0.537s 26 1.00x
🐘 Postgres Express 1.930s (+14.3% 🔺) 2.494s (+8.4% 🔺) 0.000s (-100.0% 🟢) 2.528s (+9.0% 🔺) 0.598s 24 1.06x
🐘 Postgres Next.js (Turbopack) 2.527s (-12.8% 🟢) 3.001s (-11.4% 🟢) 0.000s (-100.0% 🟢) 3.029s (-11.1% 🟢) 0.502s 20 1.39x
💻 Local Express 3.245s (-11.6% 🟢) 3.901s (-6.3% 🟢) 0.001s (+87.5% 🔺) 3.905s (-6.3% 🟢) 0.660s 16 1.78x
💻 Local Next.js (Turbopack) 3.295s (-9.7% 🟢) 3.866s (-6.2% 🟢) 0.001s (-6.2% 🟢) 3.904s (-6.4% 🟢) 0.609s 16 1.81x
💻 Local Nitro 3.323s (-11.0% 🟢) 3.967s (-6.1% 🟢) 0.000s (-18.0% 🟢) 3.970s (-6.1% 🟢) 0.647s 16 1.82x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.886s (+13.4% 🔺) 6.152s (+7.5% 🔺) 0.000s (NaN%) 6.612s (+6.3% 🔺) 1.726s 10 1.00x
▲ Vercel Express 5.031s (+17.6% 🔺) 6.310s (+15.1% 🔺) 0.000s (+22.2% 🔺) 6.931s (+17.0% 🔺) 1.900s 9 1.03x
▲ Vercel Next.js (Turbopack) 7.649s (+18.9% 🔺) 8.229s (+16.9% 🔺) 0.000s (+Infinity% 🔺) 9.275s (+13.6% 🔺) 1.626s 7 1.57x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 11/21
🐘 Postgres Nitro 17/21
▲ Vercel Express 12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 15/21
Next.js (Turbopack) 🐘 Postgres 14/21
Nitro 🐘 Postgres 20/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Comment thread packages/world-local/src/storage/events-storage.ts
Resolve conflicts in @workflow/world-local where main's hook
dedup-recovery rework landed on the same event-write paths this
branch caches:

- fs.ts writeExclusive: combine main's temp-file + hard-link atomic
  publish (with Windows retry) with this branch's withEnsuredDirectory
  ENOENT-retry wrapper, mirroring the already-merged write().
- events-storage.ts: keep main's per-instance stepLocks/hookLocks and
  the writeExclusive-based publish + canonical-eventId dedup recovery,
  and add the recent-event cache on top. Extract the cache bookkeeping
  into rememberStoredEvent() and call it after each successful publish
  (main event path + hook_conflict path) so caching layers onto main's
  stronger write semantics instead of replacing them.

Build + all 427 @workflow/world-local tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pranaygp pranaygp requested a review from ijjk as a code owner June 29, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@VaguelySerious VaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Defensive review — approve

I reviewed this with one question in mind: does it meaningfully widen the error surface or hurt debugability? My conclusion is no, and the design goes out of its way to avoid both. Approving.

Why the risk is contained

Blast radius is local-dev only. Every change is in @workflow/world-local, the filesystem backend used for local dev and tests. Production (world-vercel) is untouched, so even a latent bug here cannot reach deployed workflows.

Disk stays the single source of truth for existence. The event cache is only consulted for fileIds that listJSONFiles() actually returns from the directory listing (paginatedFileSystemQuery). So the cache can never resurrect a deleted event or invent one — it only avoids re-reading the content of files that are write-once and append-only. A lingering cache entry for a since-deleted file is simply never looked up.

Cached snapshots are genuinely detached, and proven so. rememberStoredEvent decodes from the exact serialized byte payload captured before the write await, and reads structuredClone the value out again. That closes both the caller-mutation window (test: "reuses locally appended events without exposing cached instances") and the shape-divergence risk — the reparse means a cached event is byte-for-byte what a disk read would have produced, including Date→ISO-string normalization in executionContext (test: "normalizes cached event metadata the same way as disk reads").

Memory is bounded several ways. 4 MiB / 1000 entries with FIFO eviction, oversized events bypass the cache, terminal runs release their history, and clear()/close() release everything. Per-instance, so instances can't pollute each other.

External directory removal is handled, not assumed away. withEnsuredDirectory retries exactly once on ENOENT after forgetting the cached dir — bounded (no loop), and a genuine ENOENT still propagates with its original error. All in-process directory-removal paths (clear()) are paired with clearCreatedFilesCache(); external removal is covered by the retry. Covered by three fs.test.ts regressions.

Debugability

Read-through/write-through to disk is preserved, so the JSON files on disk always reflect runtime state and remain fully inspectable. A cache parse failure silently degrades to a disk read (where the existing malformed-JSON warning surfaces) rather than masking anything.

Verification

  • pnpm test (world-local): 427 passed
  • pnpm typecheck: clean
  • CI: Unit Tests pass on ubuntu + windows. The two red checks are E2E Vercel Prod Tests (fastify) (a world-vercel deploy-run failure — "status":"failed" on the polling lane) and the Required-Check gate that aggregates it. This PR cannot affect a world-vercel prod lane; it matches the known prod-lane flakiness. Re-run rather than treat as a regression.

Minor, non-blocking notes

  • structuredClone runs per cache hit (≤1000 per large replay). The posted measurements still show a net win, and total retained bytes are capped, so this is fine — just noting it's the cost traded for mutation safety.
  • One narrow debugability tradeoff: if you hand-edit an event JSON in place for an active (non-terminal) run while the dev server is live, the next events.list() would serve the cached pre-edit content (deletion is safe — the dir listing drops it). Events are write-once by design so this is exotic, but it's the one case where on-disk state and served state can diverge during an active run.
  • createdDirectoriesCache is process-global and grows with distinct directories touched, but that set is tiny/fixed and strictly less of a concern than the pre-existing createdFilesCache (which grows per file).

Nicely scoped, well-tested, and the correctness footguns of caching are each addressed explicitly. 👍

@TooTallNate TooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — the relative-dataDir cache bug is fixed, and the regression test genuinely guards it

Re-review of d175b349f + f9c47dac9 (plus the fresh main merge). Every item from my prior REQUEST_CHANGES is addressed and verified.

The bug fix is correct and complete

The fix resolves directory once at the top of paginatedFileSystemQuery (const resolvedDirectory = path.resolve(directory)) and uses it for both the file listing and the cache-key path.join(resolvedDirectory, ...). I traced both sides of the cache to confirm the keys now agree:

  • Write side populates via cacheEvent(eventPath, ...) where eventPath = taggedPath(basedir, 'events', ...)resolveWithinBasepath.resolve(basedir, ...), which was already absolute.
  • Read side was the sole culprit: it used path.join(directory, ...) (relative when dataDir is relative), so .get(filePath) never matched the absolute key the write side set. The fix makes the read key absolute too — so write and read now produce identical keys regardless of relative/absolute dataDir.

So the cache is no longer a silent no-op in the default .workflow-data config, and the perf claim now holds in production.

The regression test actually catches the bug — I verified by reverting

The new reuses sequential-step events with a relative data directory test builds the world with path.relative(process.cwd(), testDir), runs 5 sequential steps, and asserts zero event-file readFile calls. To confirm it's a real guard, I reverted just the path.resolve fix locally and reran it:

× reuses sequential-step events with a relative data directory
  AssertionError: expected [...] to have a length of +0 but got 55

55 event-file reads without the fix (the cache-no-op), 0 with it. That's exactly the failure mode I originally reproduced, now pinned.

The optional test gaps and mock cleanup are all addressed

  • listByCorrelationId cache (my #2): reuses locally appended events for correlation queries — spies readFile, asserts 0 event-file reads on the correlation path.
  • FIFO/byte eviction (my #3): evicts old events once the recent-event byte bound is exceeded writes 1 MiB-payload events to overflow the 4 MiB cap, then asserts a subsequent read does hit disk (eventFileReads.length > 0) with correct data — exercising the while eviction loop without a slow 1000-event test. Good lightweight approach.
  • Mock restoration (my #4): the suite has a top-level afterEach(() => vi.restoreAllMocks()) (line 154), so every vi.spyOn(fs, 'readFile') is restored between tests.

Verified locally

  • Full @workflow/world-local suite: 427 passed (up from 375 at my first review — the new coverage).
  • Reverted-fix reproduction confirms the regression test fails without the change; restored → green.
  • Clean merge with current main (0 conflicts).

CI: the fastify Vercel-prod E2E failure is unrelated — this PR touches only world-local, while that lane runs against world-vercel, so it can't be affected (it's the recurring Vercel-prod flake). Worth a re-run.

Everything I flagged is resolved and the non-cache parts (createdDirectoriesCache + ENOENT retry, terminal-run cache release, structuredClone mutation isolation, clear()/close() integration) were already solid. Clean perf improvement now. LGTM.

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2707. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants