Skip to content

fix(core,errors): classify SDK encryption failures as RUNTIME_ERROR#2145

Merged
TooTallNate merged 7 commits into
mainfrom
nate/runtime-decryption-error
May 29, 2026
Merged

fix(core,errors): classify SDK encryption failures as RUNTIME_ERROR#2145
TooTallNate merged 7 commits into
mainfrom
nate/runtime-decryption-error

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

Summary

Workflow runs that fail inside the SDK's AES-GCM encryption layer were being misclassified as USER_ERROR. SDK-level decryption is never user code — the user never directly invokes subtle.decrypt — so failures here should be RUNTIME_ERROR.

This PR adds a new error class and wires it into the run-failure classifier, without addressing the root cause of the decryption failure itself (that investigation is ongoing). The change is intentionally narrow: better classification + diagnostic context, so the next mystery report is properly categorized and immediately actionable.

What I observed

Production reports surface as:

[Workflow] Error while running workflow {
  workflowRunId: 'wrun_01KSG6WQKPKNMH37C401KYHQ9M',
  errorCode: 'USER_ERROR',
  errorName: 'OperationError',
  errorStack: 'OperationError: The operation failed for an operation-specific reason\n' +
    '    at AESCipherJob.onDone (node:internal/crypto/util:646:19)'
}

OperationError from AESCipherJob.onDone is what Node's Web Crypto API throws when an AES-GCM auth-tag verification fails. The bare native DOMException doesn't match any of classifyRunError's RUNTIME_ERROR_CHECKS (which are name-based duck checks), so it falls through to USER_ERROR.

Changes

@workflow/errors (packages/errors/src/index.ts):

  • New RUNTIME_DECRYPTION_FAILED slug.
  • New RuntimeDecryptionError (extends WorkflowRuntimeError) with optional structured context (operation, byteLength, formatPrefix).

@workflow/core:

  • packages/core/src/encryption.ts: wrap both encrypt() and decrypt() Web Crypto calls; rewrap any failure as RuntimeDecryptionError with diagnostic context (printable or hex prefix of the input header, byte length, operation). The existing length-precheck now also throws RuntimeDecryptionError.
  • packages/core/src/serialization/encryption.ts & packages/core/src/serialization.ts: the two "encrypted-but-no-key" throw paths now use RuntimeDecryptionError.
  • packages/core/src/classify-error.ts: RuntimeDecryptionError.is added to RUNTIME_ERROR_CHECKS so classifyRunError routes these failures to RUNTIME_ERROR.

Test coverage

  • packages/errors/src/runtime-decryption-error.test.ts (new, 6 tests): name, inheritance, docs URL, cause preservation, context shape, name-based is() duck check.
  • packages/core/src/encryption.test.ts (new, 8 tests): happy-path round-trip, length-check failure, GCM auth-tag tamper → RuntimeDecryptionError (cause = OperationError), wrong-key decryption → same, encrypt-only key used for encrypt → RuntimeDecryptionError, printable + hex format-prefix capture.
  • packages/core/src/classify-error.test.ts (extended): RuntimeDecryptionError → RUNTIME_ERROR, plus a documentation test that a bare native OperationError still classifies as USER_ERROR (proves the encryption module's wrap is what does the work).

All existing tests still pass:

  • @workflow/errors: 36/36 ✅
  • @workflow/core: 1024/1024 ✅
  • pnpm typecheck (full repo): 40/40 packages ✅

What this does NOT fix

The actual decryption failure. Root cause is still under investigation — see the prior analysis. Strongest current hypothesis remains transport-level corruption/truncation of ciphertext between storage and read (in particular the workflow-server /refs endpoint, where a guard against truncated bodies was prototyped on a feature branch but never landed on main).

The diagnostic context added here is specifically what we need to triangulate the source on the next occurrence: byte length distinguishes "truncated" from "tampered", and format prefix distinguishes "valid encr envelope with bad ciphertext" from "garbage bytes that happened to land in a decrypt path".

SDK-level AES-GCM encrypt/decrypt failures are never the user's fault,
but the run-failure classifier was tagging them as USER_ERROR because
the native Web Crypto OperationError (most commonly raised by
AESCipherJob.onDone on GCM auth-tag mismatch) does not match any
RUNTIME_ERROR_CHECKS entry.

Introduce a new RuntimeDecryptionError (subclass of WorkflowRuntimeError)
that the encryption module throws when subtle.encrypt/subtle.decrypt
fails, with the original DOMException as cause plus diagnostic context
(operation, byteLength, printable/hex format prefix of the input
header). classifyRunError now picks it up via RUNTIME_ERROR_CHECKS, so
these failures surface as RUNTIME_ERROR with a proper named class for
dashboards and triage.
Copilot AI review requested due to automatic review settings May 28, 2026 21:26
@TooTallNate TooTallNate requested a review from a team as a code owner May 28, 2026 21:26
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: 17b9b57

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

This PR includes changesets to release 20 packages
Name Type
@workflow/errors Patch
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-vercel Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/world-testing 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
Copy link
Copy Markdown
Contributor

vercel Bot commented May 28, 2026

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 May 29, 2026 6:09pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 29, 2026 6:09pm
example-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-astro-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-express-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-fastify-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-hono-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-nitro-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-nuxt-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-sveltekit-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workbench-vite-workflow Ready Ready Preview, Comment May 29, 2026 6:09pm
workflow-docs Ready Ready Preview, Comment, Open in v0 May 29, 2026 6:09pm
workflow-swc-playground Ready Ready Preview, Comment May 29, 2026 6:09pm
workflow-tarballs Ready Ready Preview, Comment May 29, 2026 6:09pm
workflow-web Ready Ready Preview, Comment May 29, 2026 6:09pm

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1255 0 219 1474
✅ 💻 Local Development 1657 0 219 1876
✅ 📦 Local Production 1657 0 219 1876
✅ 🐘 Local Postgres 1657 0 219 1876
✅ 🪟 Windows 134 0 0 134
✅ 📋 Other 762 0 176 938
Total 7122 0 1052 8174

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 108 0 26
✅ example 108 0 26
✅ express 108 0 26
✅ fastify 108 0 26
✅ hono 108 0 26
✅ nextjs-turbopack 132 0 2
✅ nextjs-webpack 132 0 2
✅ nitro 108 0 26
✅ nuxt 108 0 26
✅ sveltekit 127 0 7
✅ vite 108 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 109 0 25
✅ express-stable 109 0 25
✅ fastify-stable 109 0 25
✅ hono-stable 109 0 25
✅ nextjs-turbopack-canary 115 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 134 0 0
✅ nextjs-webpack-canary 115 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 134 0 0
✅ nitro-stable 109 0 25
✅ nuxt-stable 109 0 25
✅ sveltekit-stable 128 0 6
✅ vite-stable 109 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 109 0 25
✅ express-stable 109 0 25
✅ fastify-stable 109 0 25
✅ hono-stable 109 0 25
✅ nextjs-turbopack-canary 115 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 134 0 0
✅ nextjs-webpack-canary 115 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 134 0 0
✅ nitro-stable 109 0 25
✅ nuxt-stable 109 0 25
✅ sveltekit-stable 128 0 6
✅ vite-stable 109 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 109 0 25
✅ express-stable 109 0 25
✅ fastify-stable 109 0 25
✅ hono-stable 109 0 25
✅ nextjs-turbopack-canary 115 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 134 0 0
✅ nextjs-webpack-canary 115 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 134 0 0
✅ nitro-stable 109 0 25
✅ nuxt-stable 109 0 25
✅ sveltekit-stable 128 0 6
✅ vite-stable 109 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 134 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 109 0 25
✅ e2e-local-dev-tanstack-start- 109 0 25
✅ e2e-local-postgres-nest-stable 109 0 25
✅ e2e-local-postgres-tanstack-start- 109 0 25
✅ e2e-local-prod-nest-stable 109 0 25
✅ e2e-local-prod-tanstack-start- 109 0 25
✅ e2e-vercel-prod-tanstack-start 108 0 26

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

📊 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
💻 Local 🥇 Nitro 0.041s (-6.0% 🟢) 1.005s (~) 0.965s 10 1.00x
💻 Local Express 0.044s (~) 1.006s (~) 0.962s 10 1.09x
🐘 Postgres Next.js (Turbopack) 0.061s 1.011s 0.950s 10 1.50x
🐘 Postgres Nitro 0.065s (-31.9% 🟢) 1.012s (-3.0%) 0.947s 10 1.60x
🐘 Postgres Express 0.067s (+16.0% 🔺) 1.012s (~) 0.945s 10 1.66x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.279s (+10.9% 🔺) 2.546s (+9.1% 🔺) 2.267s 10 1.00x
▲ Vercel Express 0.311s (+32.3% 🔺) 2.196s (+2.8%) 1.885s 10 1.12x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.096s (-3.1%) 2.006s (~) 0.909s 10 1.00x
💻 Local Express 1.097s (-2.5%) 2.006s (~) 0.909s 10 1.00x
🐘 Postgres Express 1.104s (-3.7%) 2.010s (~) 0.905s 10 1.01x
🐘 Postgres Nitro 1.111s (-2.6%) 2.009s (~) 0.898s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.121s 2.010s 0.889s 10 1.02x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.683s (-17.3% 🟢) 3.502s (-8.6% 🟢) 1.820s 10 1.00x
▲ Vercel Express 1.745s (-6.9% 🟢) 3.604s (-5.3% 🟢) 1.859s 10 1.04x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.518s (-3.9%) 11.022s (~) 0.504s 3 1.00x
💻 Local Express 10.533s (-3.6%) 11.023s (~) 0.491s 3 1.00x
🐘 Postgres Nitro 10.534s (-3.1%) 11.018s (~) 0.484s 3 1.00x
🐘 Postgres Express 10.555s (-3.7%) 11.019s (~) 0.464s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.668s 11.018s 0.350s 3 1.01x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.546s (-20.2% 🟢) 15.595s (-22.1% 🟢) 2.050s 2 1.00x
▲ Vercel Next.js (Turbopack) 14.112s (-18.5% 🟢) 15.890s (-18.1% 🟢) 1.778s 2 1.04x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.745s (-8.7% 🟢) 14.026s (-12.5% 🟢) 0.281s 5 1.00x
💻 Local Express 13.828s (-7.6% 🟢) 14.027s (-6.7% 🟢) 0.200s 5 1.01x
🐘 Postgres Nitro 13.886s (-4.9%) 14.020s (-6.7% 🟢) 0.134s 5 1.01x
🐘 Postgres Express 13.886s (-4.8%) 14.022s (-6.7% 🟢) 0.135s 5 1.01x
🐘 Postgres Next.js (Turbopack) 14.045s 14.815s 0.770s 5 1.02x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 22.168s (-55.9% 🟢) 23.750s (-54.8% 🟢) 1.582s 3 1.00x
▲ Vercel Next.js (Turbopack) 22.812s (-56.6% 🟢) 24.730s (-54.7% 🟢) 1.918s 3 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 12.484s (-25.6% 🟢) 13.026s (-23.5% 🟢) 0.542s 7 1.00x
🐘 Postgres Nitro 12.487s (-10.6% 🟢) 13.020s (-9.0% 🟢) 0.533s 7 1.00x
💻 Local Express 12.550s (-24.4% 🟢) 13.025s (-23.5% 🟢) 0.476s 7 1.01x
🐘 Postgres Express 12.590s (-10.1% 🟢) 13.026s (-10.7% 🟢) 0.435s 7 1.01x
🐘 Postgres Next.js (Turbopack) 13.042s 13.588s 0.545s 7 1.04x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 31.487s (-74.0% 🟢) 33.795s (-72.7% 🟢) 2.308s 3 1.00x
▲ Vercel Next.js (Turbopack) 33.506s (-91.5% 🟢) 36.264s (-90.8% 🟢) 2.758s 3 1.06x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.182s (-7.3% 🟢) 2.008s (~) 0.826s 15 1.00x
🐘 Postgres Express 1.190s (-5.6% 🟢) 2.007s (~) 0.817s 15 1.01x
💻 Local Nitro 1.202s (-26.3% 🟢) 2.006s (-3.3%) 0.804s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.209s 2.008s 0.799s 15 1.02x
💻 Local Express 1.245s (-16.4% 🟢) 2.006s (~) 0.761s 15 1.05x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.562s (-10.4% 🟢) 4.159s (-10.0% 🟢) 1.596s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.651s (-22.0% 🟢) 4.816s (-2.4%) 2.166s 7 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: 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.256s (-46.8% 🟢) 2.007s (-33.3% 🟢) 0.751s 15 1.00x
🐘 Postgres Nitro 1.258s (-46.5% 🟢) 2.007s (-33.3% 🟢) 0.749s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.317s 2.007s 0.690s 15 1.05x
💻 Local Nitro 1.901s (-39.5% 🟢) 2.316s (-40.4% 🟢) 0.416s 13 1.51x
💻 Local Express 1.908s (-35.4% 🟢) 2.315s (-33.0% 🟢) 0.407s 13 1.52x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.844s (+6.2% 🔺) 5.256s (+2.8%) 1.412s 6 1.00x
▲ Vercel Next.js (Turbopack) 4.416s (-37.8% 🟢) 6.385s (-28.3% 🟢) 1.969s 5 1.15x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.381s (-60.3% 🟢) 2.008s (-49.9% 🟢) 0.626s 15 1.00x
🐘 Postgres Express 1.392s (-60.1% 🟢) 2.008s (-49.9% 🟢) 0.615s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.601s 2.152s 0.550s 14 1.16x
💻 Local Express 5.433s (-34.8% 🟢) 6.013s (-33.4% 🟢) 0.580s 5 3.93x
💻 Local Nitro 5.622s (-32.7% 🟢) 6.214s (-31.1% 🟢) 0.592s 5 4.07x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.476s (-27.4% 🟢) 9.077s (-17.2% 🟢) 2.601s 4 1.00x
▲ Vercel Express 6.728s (+58.7% 🔺) 8.660s (+41.3% 🔺) 1.933s 4 1.04x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.183s (-5.9% 🟢) 2.008s (~) 0.826s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.205s 2.008s 0.803s 15 1.02x
🐘 Postgres Express 1.220s (-2.9%) 2.009s (~) 0.789s 15 1.03x
💻 Local Nitro 1.564s (-16.2% 🟢) 2.006s (-14.3% 🟢) 0.442s 15 1.32x
💻 Local Express 1.578s (-16.6% 🟢) 2.007s (-15.1% 🟢) 0.428s 15 1.33x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.930s (~) 4.613s (-0.6%) 1.683s 7 1.00x
▲ Vercel Express 3.044s (+17.9% 🔺) 4.348s (~) 1.305s 7 1.04x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.249s (-46.6% 🟢) 2.008s (-33.3% 🟢) 0.758s 15 1.00x
🐘 Postgres Express 1.264s (-46.0% 🟢) 2.008s (-33.3% 🟢) 0.745s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.330s 2.007s 0.677s 15 1.06x
💻 Local Nitro 2.055s (-33.0% 🟢) 2.508s (-35.5% 🟢) 0.454s 12 1.64x
💻 Local Express 2.058s (-34.3% 🟢) 2.592s (-31.1% 🟢) 0.534s 12 1.65x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.458s (+8.3% 🔺) 5.409s (+12.9% 🔺) 1.951s 6 1.00x
▲ Vercel Next.js (Turbopack) 4.283s (+36.3% 🔺) 6.152s (+36.0% 🔺) 1.868s 5 1.24x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.392s (-60.2% 🟢) 2.008s (-49.9% 🟢) 0.616s 15 1.00x
🐘 Postgres Nitro 1.395s (-59.9% 🟢) 2.007s (-49.9% 🟢) 0.612s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.579s 2.075s 0.496s 15 1.13x
💻 Local Nitro 6.250s (-31.7% 🟢) 6.815s (-32.0% 🟢) 0.565s 5 4.49x
💻 Local Express 6.414s (-27.1% 🟢) 6.817s (-26.5% 🟢) 0.404s 5 4.61x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.776s (-10.0% 🟢) 7.567s (-7.5% 🟢) 1.792s 4 1.00x
▲ Vercel Next.js (Turbopack) 5.932s (-12.2% 🟢) 8.092s (-5.3% 🟢) 2.160s 4 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | 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.585s (-28.8% 🟢) 1.023s (+1.7%) 0.439s 59 1.00x
🐘 Postgres Express 0.599s (-28.6% 🟢) 1.024s (~) 0.425s 59 1.03x
💻 Local Nitro 0.621s (-36.7% 🟢) 1.005s (-8.1% 🟢) 0.384s 60 1.06x
💻 Local Express 0.639s (-35.1% 🟢) 1.022s (-5.1% 🟢) 0.383s 59 1.09x
🐘 Postgres Next.js (Turbopack) 0.656s 1.006s 0.350s 60 1.12x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.540s (-61.8% 🟢) 7.384s (-54.1% 🟢) 1.844s 9 1.00x
▲ Vercel Express 5.991s (-68.5% 🟢) 7.568s (-64.5% 🟢) 1.577s 8 1.08x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.407s (-27.0% 🟢) 2.054s (-2.2%) 0.647s 44 1.00x
🐘 Postgres Express 1.421s (-28.1% 🟢) 2.030s (-10.1% 🟢) 0.610s 45 1.01x
💻 Local Nitro 1.534s (-49.5% 🟢) 2.007s (-46.6% 🟢) 0.473s 45 1.09x
💻 Local Express 1.567s (-48.0% 🟢) 2.028s (-43.4% 🟢) 0.461s 45 1.11x
🐘 Postgres Next.js (Turbopack) 1.582s 2.029s 0.447s 45 1.12x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 13.438s (-73.0% 🟢) 15.529s (-70.0% 🟢) 2.091s 6 1.00x
▲ Vercel Express 112.137s (+224.8% 🔺) 114.470s (+211.0% 🔺) 2.333s 3 8.34x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.683s (-34.6% 🟢) 3.058s (-33.6% 🟢) 0.375s 40 1.00x
🐘 Postgres Express 2.761s (-30.8% 🟢) 3.112s (-28.8% 🟢) 0.350s 39 1.03x
🐘 Postgres Next.js (Turbopack) 3.174s 4.009s 0.834s 30 1.18x
💻 Local Express 3.268s (-64.5% 🟢) 4.009s (-60.0% 🟢) 0.741s 30 1.22x
💻 Local Nitro 3.323s (-64.3% 🟢) 4.043s (-59.6% 🟢) 0.720s 30 1.24x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 25.848s (-80.1% 🟢) 27.966s (-78.8% 🟢) 2.117s 5 1.00x
▲ Vercel Next.js (Turbopack) 29.200s (-72.7% 🟢) 31.692s (-70.9% 🟢) 2.492s 4 1.13x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: 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.217s (-23.4% 🟢) 1.006s (~) 0.789s 60 1.00x
🐘 Postgres Express 0.225s (-20.2% 🟢) 1.006s (~) 0.780s 60 1.04x
🐘 Postgres Next.js (Turbopack) 0.235s 1.006s 0.771s 60 1.08x
💻 Local Express 0.470s (-16.1% 🟢) 1.021s (+1.7%) 0.551s 59 2.17x
💻 Local Nitro 0.473s (-21.9% 🟢) 1.022s (~) 0.549s 59 2.18x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.215s (+13.4% 🔺) 3.876s (+6.6% 🔺) 1.661s 16 1.00x
▲ Vercel Next.js (Turbopack) 2.347s (+16.0% 🔺) 4.082s (+7.6% 🔺) 1.735s 15 1.06x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | 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.344s (-30.7% 🟢) 1.006s (~) 0.662s 90 1.00x
🐘 Postgres Express 0.365s (-28.5% 🟢) 1.007s (~) 0.642s 90 1.06x
🐘 Postgres Next.js (Turbopack) 0.424s 1.006s 0.582s 90 1.23x
💻 Local Express 2.116s (-15.8% 🟢) 2.657s (-11.7% 🟢) 0.540s 34 6.15x
💻 Local Nitro 2.163s (-14.8% 🟢) 2.821s (-6.2% 🟢) 0.658s 32 6.29x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.343s (+75.4% 🔺) 7.136s (+48.4% 🔺) 1.793s 13 1.00x
▲ Vercel Next.js (Turbopack) 5.372s (+52.0% 🔺) 7.161s (+37.9% 🔺) 1.789s 13 1.01x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | 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.697s (-11.9% 🟢) 1.006s (~) 0.310s 120 1.00x
🐘 Postgres Express 0.706s (-13.8% 🟢) 1.006s (-1.1%) 0.301s 120 1.01x
🐘 Postgres Next.js (Turbopack) 0.800s 1.006s 0.206s 120 1.15x
💻 Local Express 9.994s (-10.7% 🟢) 10.443s (-12.5% 🟢) 0.449s 12 14.35x
💻 Local Nitro 10.322s (-7.8% 🟢) 10.938s (-6.2% 🟢) 0.617s 11 14.82x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.712s (+84.8% 🔺) 15.511s (+67.8% 🔺) 1.798s 8 1.00x
▲ Vercel Next.js (Turbopack) 14.076s (+36.3% 🔺) 16.036s (+30.5% 🔺) 1.960s 8 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: 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
💻 Local 🥇 Nitro 1.164s (+444.7% 🔺) 2.005s (+99.6% 🔺) 0.013s (+0.8%) 2.020s (+98.2% 🔺) 0.856s 10 1.00x
💻 Local Express 1.166s (+485.4% 🔺) 2.005s (+99.6% 🔺) 0.012s (+1.7%) 2.019s (+98.3% 🔺) 0.854s 10 1.00x
🐘 Postgres Express 1.167s (+468.8% 🔺) 2.000s (+100.2% 🔺) 0.002s (+6.2% 🔺) 2.011s (+98.9% 🔺) 0.845s 10 1.00x
🐘 Postgres Nitro 1.172s (+471.8% 🔺) 1.996s (+99.6% 🔺) 0.002s (+13.3% 🔺) 2.011s (+98.8% 🔺) 0.839s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.181s 2.001s 0.001s 2.009s 0.828s 10 1.01x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.333s (-66.0% 🟢) 3.220s (-62.8% 🟢) 1.844s (+191.8% 🔺) 5.539s (-43.4% 🟢) 3.206s 10 1.00x
▲ Vercel Express 2.421s (-3.4%) 3.283s (-19.7% 🟢) 2.090s (+117.5% 🔺) 5.842s (+4.5%) 3.421s 10 1.04x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.583s (+153.6% 🔺) 2.006s (+99.3% 🔺) 0.004s (-8.1% 🟢) 2.024s (+98.0% 🔺) 0.441s 30 1.00x
💻 Local Express 1.598s (+111.1% 🔺) 2.010s (+95.3% 🔺) 0.010s (+4.5%) 2.022s (+94.4% 🔺) 0.423s 30 1.01x
💻 Local Nitro 1.617s (+92.8% 🔺) 2.010s (+98.6% 🔺) 0.011s (+17.4% 🔺) 2.023s (+81.3% 🔺) 0.406s 30 1.02x
🐘 Postgres Express 1.624s (+157.8% 🔺) 2.004s (+99.1% 🔺) 0.004s (+0.9%) 2.026s (+98.1% 🔺) 0.402s 30 1.03x
🐘 Postgres Next.js (Turbopack) 1.660s 2.009s 0.003s 2.024s 0.364s 30 1.05x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.242s (-63.1% 🟢) 7.679s (-57.9% 🟢) 0.193s (-8.7% 🟢) 8.404s (-55.6% 🟢) 2.162s 8 1.00x
▲ Vercel Express 6.715s (+3.2%) 8.593s (+7.3% 🔺) 0.441s (+7.8% 🔺) 9.531s (+7.9% 🔺) 2.817s 7 1.08x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.744s (-22.6% 🟢) 1.047s (-18.0% 🟢) 0.000s (-19.3% 🟢) 1.060s (-18.8% 🟢) 0.317s 57 1.00x
🐘 Postgres Nitro 0.755s (-22.0% 🟢) 1.083s (-13.2% 🟢) 0.000s (-15.8% 🟢) 1.096s (-12.9% 🟢) 0.340s 57 1.02x
🐘 Postgres Next.js (Turbopack) 0.770s 1.073s 0.000s 1.091s 0.321s 55 1.04x
💻 Local Express 1.401s (+14.4% 🔺) 2.013s (~) 0.000s (-10.0% 🟢) 2.015s (~) 0.614s 30 1.88x
💻 Local Nitro 1.453s (+18.8% 🔺) 2.014s (~) 0.000s (+300.0% 🔺) 2.017s (~) 0.564s 30 1.95x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.723s (-63.4% 🟢) 5.329s (-53.7% 🟢) 0.000s (NaN%) 5.990s (-50.3% 🟢) 2.267s 11 1.00x
▲ Vercel Express 4.009s (+7.2% 🔺) 5.328s (+4.4%) 0.000s (-50.0% 🟢) 5.759s (+4.1%) 1.750s 11 1.08x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.444s (-18.5% 🟢) 2.100s (-3.6%) 0.000s (NaN%) 2.115s (-3.8%) 0.671s 29 1.00x
🐘 Postgres Nitro 1.460s (-18.5% 🟢) 2.063s (-3.7%) 0.000s (-3.4%) 2.079s (-4.4%) 0.618s 29 1.01x
🐘 Postgres Next.js (Turbopack) 1.591s 2.180s 0.000s 2.199s 0.608s 28 1.10x
💻 Local Express 3.093s (-10.8% 🟢) 3.900s (-3.3%) 0.000s (-60.9% 🟢) 3.903s (-3.3%) 0.809s 16 2.14x
💻 Local Nitro 3.731s (+10.1% 🔺) 4.028s (~) 0.001s (+74.1% 🔺) 4.389s (+8.7% 🔺) 0.658s 14 2.58x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.296s (+37.3% 🔺) 7.469s (+24.0% 🔺) 0.001s (+Infinity% 🔺) 7.917s (+22.6% 🔺) 1.621s 8 1.00x
▲ Vercel Next.js (Turbopack) 20.436s (+263.8% 🔺) 21.771s (+211.8% 🔺) 0.000s (+100.0% 🔺) 22.320s (+196.0% 🔺) 1.884s 8 3.25x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 13/21
🐘 Postgres Nitro 14/21
▲ Vercel Express 12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 14/21
Next.js (Turbopack) 🐘 Postgres 21/21
Nitro 🐘 Postgres 15/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Workflow run failures originating in the SDK's AES-GCM encryption layer (most notably Node's native OperationError from AESCipherJob.onDone on GCM auth-tag mismatch) were falling through to USER_ERROR because classifyRunError's name-based duck checks didn't match a raw DOMException. This PR introduces a RuntimeDecryptionError (subclass of WorkflowRuntimeError) that the encryption module always wraps Web Crypto failures in, plus diagnostic context (operation, byte length, header prefix), so failures classify as RUNTIME_ERROR and carry enough telemetry to triangulate root cause on the next occurrence. No root-cause fix is attempted.

Changes:

  • New RuntimeDecryptionError class + runtime-decryption-failed slug in @workflow/errors with optional structured context.
  • Wrap encrypt/decrypt Web Crypto calls in packages/core/src/encryption.ts and rewrap the two "encrypted-but-no-key" throws in serialization paths.
  • Add RuntimeDecryptionError.is to RUNTIME_ERROR_CHECKS and cover the new behavior with errors + core tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/errors/src/index.ts Adds RUNTIME_DECRYPTION_FAILED slug and RuntimeDecryptionError class with name-based .is().
packages/errors/src/runtime-decryption-error.test.ts New tests covering name, docs link, cause, context shape, and .is() duck check.
packages/core/src/encryption.ts Wraps subtle.encrypt/decrypt failures and length precheck in RuntimeDecryptionError; adds printable/hex diagnostic prefix helper.
packages/core/src/encryption.test.ts New 8-test module: round-trip, length-check, tamper, wrong key, encrypt-only-usage, prefix capture.
packages/core/src/serialization/encryption.ts Switches "encrypted-but-no-key" throw to RuntimeDecryptionError with context.
packages/core/src/serialization.ts Same switch on the deserialize-stream path.
packages/core/src/classify-error.ts Adds RuntimeDecryptionError.is to RUNTIME_ERROR_CHECKS.
packages/core/src/classify-error.test.ts Adds tests for the new mapping and a bare-OperationError sanity check.
.changeset/runtime-decryption-error.md Patch changeset for @workflow/errors and @workflow/core.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/errors/src/index.ts
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

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

Left three inline findings from local verification.

Comment thread packages/errors/src/index.ts
Comment thread packages/core/src/encryption.ts Outdated
Comment thread packages/core/src/encryption.ts
@pranaygp
Copy link
Copy Markdown
Contributor

Follow-up thought after tracing the runtime path: I think this PR is appropriately scoped to improving attribution (RUNTIME_ERROR rather than USER_ERROR), and it should not be required to solve retry behavior as part of this change.

That said, we should follow up by applying the same bounded-redelivery precedent used for replay timeouts to RuntimeDecryptionErrors encountered while replaying remotely fetched persisted data. An AES-GCM authentication failure is terminal for the bytes/key in the current attempt, so we must not continue execution; but if the bytes came from a transiently truncated or corrupted /refs response, a fresh queue delivery can re-fetch them successfully. Today we commit run_failed immediately, which turns a potentially recoverable read failure into a terminal workflow failure.

Concretely, for managed worlds we should let the queue redrive a small bounded number of times (re-fetching the events/ref payload each delivery), then commit terminal run_failed as RUNTIME_ERROR if the decryption failure persists. Longer term, detecting response truncation or integrity failure at the /refs transport boundary would let us classify the retryable case more directly. This feels like a focused follow-up PR rather than a blocker for the attribution fix here.

…x, propagate through serialization wrappers

Addresses review feedback on #2145:

- Add a RuntimeDecryptionError reducer/reviver (+ SerializableSpecial
  entry + globalThis registration) so its `context` (operation,
  byteLength, formatPrefix) survives the dehydrate/hydrate run-error
  round trip instead of being dropped by the generic Error reducer.

- Stop capturing `formatPrefix` in the low-level encryption layer, which
  only sees the stripped AES payload (nonce bytes), not the outer `encr`
  marker. The serialization layer now attaches the real envelope prefix.

- Rethrow RuntimeDecryptionError unchanged from the serialize/dehydrate
  catch blocks instead of reframing it as a SerializationError, so an
  encryption failure during dehydration stays a RUNTIME_ERROR rather than
  being misclassified as USER_ERROR.
@TooTallNate
Copy link
Copy Markdown
Member Author

Agreed on both points — keeping this PR scoped to attribution, and treating bounded redelivery as a focused follow-up.

The reasoning is sound: an AES-GCM auth failure is terminal for the current bytes/key, but if those bytes came from a transiently truncated/corrupted /refs response, a fresh queue delivery can re-fetch and succeed. Committing run_failed immediately turns a recoverable read failure into a terminal one.

I'll open a follow-up to apply the bounded-redelivery precedent (the same one used for replay timeouts) to RuntimeDecryptionErrors encountered while replaying remotely-fetched persisted data on managed worlds: redrive a small bounded number of times (re-fetching the events/ref payload each delivery), then commit terminal run_failed as RUNTIME_ERROR if it persists. The RuntimeDecryptionError class + diagnostic context landed here give that follow-up a clean signal to branch on, and the longer-term /refs transport-boundary integrity check would let us classify the retryable case even more directly.

The review feedback on this PR has been addressed in the latest commits:

  • RuntimeDecryptionError.context now round-trips through dehydrateRunError/hydrateRunError (reducer/reviver + globalThis registration).
  • formatPrefix is captured at the serialization layer (real encr marker) instead of the low-level layer (which only saw nonce bytes).
  • Encrypt-side failures now propagate as RuntimeDecryptionError through the dehydrate wrappers instead of being reframed as SerializationError (→ USER_ERROR).

Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

AI review: no blocking issues

Comment thread packages/core/src/serialization.ts
- Mirror the catch/enrich/rethrow block from serialization/encryption.ts
  around the stream-path aesGcmDecrypt() call so auth-tag failures on
  encrypted stream frames also carry context.formatPrefix = 'encr'
  (addresses review feedback). Add a tampered-frame test.
- Fix all auto-fixable Biome lint findings in the touched files
  (template literals, useless try/catch wrappers, optional chaining,
  non-null assertions).
@github-actions
Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2165. 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.

4 participants