Skip to content

[world-local] Fix per-step AbortSignal latency and O(world) chunk polling#2807

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/issue-2795-2797
Jul 8, 2026
Merged

[world-local] Fix per-step AbortSignal latency and O(world) chunk polling#2807
VaguelySerious merged 10 commits into
mainfrom
peter/issue-2795-2797

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Jul 7, 2026

Copy link
Copy Markdown
Member

Fixes two companion issues about local-world stream performance. Closes #2795. Closes #2797.

#2795 — AbortSignal in a step adds ~0.5s fixed latency per step

Passing an AbortSignal into a step's arguments cost a flat ~500ms per invocation on world-local (a ~7× slowdown for a trivial step).

Root cause. Reviving a (non-aborted) AbortSignal on the step side opens a real-time abort-stream reader for the step's duration and pushes its read() into the step's ops. The inline step executor (executeStep) never cancelled that reader after user code — so the read() (on a stream that has no chunks and never will) never settled, the post-step ops-settle check always lost its 500ms race, the step reported hasPendingOps, and the inline loop queued a continuation. Result: one full queue round-trip per signal-bearing step instead of running inline.

Fix.

  • executeStep now calls cancelAbortReaders(...) after user code, matching what the non-inline step-handler path already did. The reader settles immediately and the step runs inline again.
  • setupAbortStreamReader now cancels (rather than only releasing the lock on) the reader, so a polling world tears down its tail reader instead of leaking one per step.

Measured on the world-testing embedded harness (20 sequential trivial steps): +521ms/step → −4ms/step, and flow invocations for a 10-step signal workflow 11 → 1.

#2797 — tail readers poll with a full readdir of the global chunks dir

Every open world-local tail reader re-listed the entire world-global streams/chunks/ directory every 100ms (O(total chunks in the world) per tick per reader), then prefix-filtered. Combined with the leaked readers from #2795, a long single-session workload degraded superlinearly.

Fix.

  • Chunks are now sharded one directory per stream (streams/chunks/<streamName>/<chunkId>.bin), so a poll lists only that stream's chunks (O(stream) instead of O(world)). An abort stream that never receives a chunk reads an empty/absent directory.
  • The reader's teardown was hardened so a cancel() reliably removes the emitter listeners and clears the poll timer even if it races start().

Testing

  • New regression test in @workflow/world-testing inline-execution suite: a signal-bearing sequential workflow completes in exactly 1 flow invocation.
  • New @workflow/world-local streamer tests: reader teardown releases listeners + timer on cancel; chunk listing is scoped to the stream (unaffected by thousands of unrelated chunks).
  • Updated existing streamer tests for the sharded layout.
  • Full @workflow/world-local (434) and @workflow/core runtime/serialization suites pass.
  • AbortController (28) and streaming (9) E2E tests pass against a local nextjs-turbopack app — confirming real-time abort propagation and cross-process stream reads still work with the new teardown and sharded layout.

Relationship to #2779 and the turn-cancellation investigation

This PR comes out of the same investigation as #2779 ("Fix hook token reuse after dispose()", closes #2777 / #2778) — debugging correctness and performance of turn cancellation on world-local (a long single-session loop that creates a durable AbortController per turn and threads its signal into steps). That investigation surfaced several independent facets, tracked as #2777, #2778, #2780, #2781, #2795, #2797:

So #2779 and #2807 are complementary fixes to the same durable-cancellation pattern: #2779 makes the hook side reusable, this PR makes the signal/stream side cheap. Neither depends on the other to land.

…ling

Two companion fixes for local-world stream performance:

- core: the inline step executor now tears down abort-stream readers after
  user code (like the non-inline path already did). A serialized AbortSignal
  opened a real-time reader whose read() never settled, so every signal-bearing
  step lost the 500ms ops-settle race, reported hasPendingOps, and took a full
  queue round-trip instead of running inline (#2795). setupAbortStreamReader now
  cancels (not just releases) the reader so a polling world doesn't leak a tail
  reader per step.

- world-local: shard stream chunks into a directory per stream so a tail
  reader's poll lists only that stream's chunks instead of the whole world's on
  every 100ms tick (#2797), and reliably release emitter listeners + poll timer
  when a reader is cancelled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e3af9d

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/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/vitest Patch
@workflow/web Patch
@workflow/world-postgres Patch
@workflow/world-testing Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
workflow Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/nuxt Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite 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 Jul 7, 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 Jul 8, 2026 1:25am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 8, 2026 1:25am
example-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-astro-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-express-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-fastify-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-hono-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-nitro-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workbench-vite-workflow Ready Ready Preview, Comment Jul 8, 2026 1:25am
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 8, 2026 1:25am
workflow-swc-playground Ready Ready Preview, Comment Jul 8, 2026 1:25am
workflow-tarballs Ready Ready Preview, Comment Jul 8, 2026 1:25am
workflow-web Ready Ready Preview, Comment Jul 8, 2026 1:25am

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1453 0 230 1683
✅ 💻 Local Development 1617 0 219 1836
✅ 📦 Local Production 1617 0 219 1836
✅ 🐘 Local Postgres 1617 0 219 1836
✅ 🪟 Windows 153 0 0 153
✅ 📋 Other 894 0 177 1071
Total 7351 0 1064 8415

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
✅ example 126 0 27
✅ express 126 0 27
✅ fastify 126 0 27
✅ hono 126 0 27
✅ nextjs-turbopack 150 0 3
✅ nextjs-webpack 150 0 3
✅ nitro 126 0 27
✅ nuxt 126 0 27
✅ sveltekit 145 0 8
✅ vite 126 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 25
✅ e2e-local-dev-tanstack-start- 128 0 25
✅ e2e-local-postgres-nest-stable 128 0 25
✅ e2e-local-postgres-tanstack-start- 128 0 25
✅ e2e-local-prod-nest-stable 128 0 25
✅ e2e-local-prod-tanstack-start- 128 0 25
✅ e2e-vercel-prod-tanstack-start 126 0 27

📋 View full workflow run

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

No benchmark result files found in benchmark-results

Comment thread packages/core/src/runtime/step-executor.ts
The previous change awaited reader.cancel() before propagating a received
abort packet. On a service-backed world (world-vercel) cancel() does a network
round-trip that can block, so awaiting it delayed — and when it stalled, dropped
— real-time abort delivery to the in-flight step (8 AbortController E2E tests
timed out on both world-vercel prod lanes). Now the abort is propagated first
via a synchronous releaseLock(), and the underlying stream is only cancelled on
the no-abort teardown path, fire-and-forget so it can't block the ops-settle
window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3 karthikscale3 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.

Verified the latency claim empirically and reviewed the mechanism — details below, plus two findings as inline comments.

What I checked:

  • A/B on the new regression workload (5 sequential signal-bearing steps, embedded local world): with this branch, 1 flow invocation, 511ms; with only step-executor.ts reverted to main (everything else identical, rebuilt), 6 invocations, 3377ms — ~575ms/step, matching the PR's ~521ms/step claim.
  • Mechanism: confirmed setupAbortStreamReader pushes a never-settling read() into ops, the inline ops-flush race times out at 500ms → hasPendingOps → queue continuation per step; cancelAbortReaders settles it via readerCancel, and the new executeStep call is identical to what step-handler already did.
  • Streamer teardown: teardown() is wired synchronously before the first await in start(), and the streamClosed re-check before arming the poll closes the cancel-during-disk-read race (the check→setInterval window is synchronous).
  • Suites locally: @workflow/world-local 436/436, core step-handler/serialization 234/234, new inline-execution test passes.
  • CI: the 2 failing checks are the known-flaky nextjs-webpack canary E2E lane + its aggregator, unrelated to this diff.

* world). A tail reader polling for new chunks would otherwise `readdir` the
* entire global chunks directory every 100ms — see vercel/workflow#2797.
*/
function chunkDirForStream(chunksBaseDir: string, name: string): string {

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.

The tag-scoped cleanup() in packages/world-local/src/index.ts (~L168-177, not in this diff so commenting here) still lists the flat streams/chunks dir via listTaggedFilesByExtension, which is a non-recursive readdir (fs.ts ~L263). After this sharding, that directory contains only per-stream subdirectories, so the .{tag}.bin filter matches nothing and tagged chunk deletion silently no-ops — the vitest plugin's per-tag cleanup now leaks chunk files across test sessions. Suggest iterating the per-stream subdirs there (and pruning emptied ones).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(AI) Good catch — fixed in dec64b3. clear() now iterates each per-stream subdirectory under streams/chunks/ and deletes the .{tag}.bin files within, instead of listing the (now subdirectory-only) top-level chunks dir. Added a regression test in tag.test.ts ("should clear tagged stream chunks in the sharded layout") that writes tagged chunks and asserts clear() removes them.

Comment thread .changeset/local-chunk-sharding.md Outdated
'@workflow/world-local': patch
---

Shard stream chunks into a directory per stream so a tail reader's poll no longer lists every chunk in the world on each tick, and reliably release its emitter listeners and poll timer when the reader is cancelled.

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.

There's no fallback for the legacy flat layout: chunks written by an older version (streams/chunks/<name>-<chunkId>.bin) become invisible to the new reader, so a run in flight across the upgrade would hang waiting on a stream it can never see, and old runs' streams won't show in workflow web. Probably an acceptable tradeoff for local dev data, but worth a sentence here (e.g. "existing .workflow-data stream chunks from older versions won't be read") — and note the old flat files are never cleaned up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(AI) Added a note to the changeset in dec64b3 spelling out the tradeoff: chunks are now under streams/chunks/<streamName>/, chunk files written by an older version to the flat layout are not read back, and stale flat files are left in place rather than cleaned up.

@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.

The two fixes are well-designed and well-tested — I verified the core reasoning and ran all the suites (world-local 436, core 1363, world-testing 11, all passing, including the new invocation-count regression test). But the chunk-layout change misses one consumer of the old flat layout, which I think should be fixed in this PR since it silently breaks an existing invariant:

Blocking: the tagged-world reset still lists the flat chunks directory (packages/world-local/src/index.ts:169). The cleanup does listTaggedFilesByExtension(chunksDir, tag, '.bin') against streams/chunks/ and unlinks matches at the top level. Post-sharding, that directory contains only per-stream subdirectories, so the readdir returns no matching files and the "Delete tagged stream chunks" step becomes a silent no-op — every tagged world reset now leaks all of its chunk data. Since vitest worker tags (e.g. vitest-0) are reused across runs against a persistent data dir, this accumulates indefinitely — which is ironic given #2797 is specifically about pathologies from chunk accumulation. The fix needs to walk the per-stream subdirectories (and ideally remove emptied stream dirs). A regression test asserting reset actually removes tagged chunks under the sharded layout would lock it in.

Everything else looks good — details of what I verified:

  • #2795 fix: executeStep's capture/cleanup/rethrow brings the inline path to parity with the handler path's existing pattern, and the follow-up commit correctly covers the throwing-step leak (per-attempt reader leak on retries). The new step-handler tests cover both success and failure paths.
  • Delivery-vs-teardown ordering in setupAbortStreamReader: releasing (sync) on the abort-arrival path so controller.abort() isn't gated on a potentially-network-bound cancel(), while cancelling (fire-and-forget) on the no-abort path to tear down polling-world tail readers — both directions are reasoned correctly, and the no-abort path is the one that matters for the leak.
  • Sharding: write/read/poll/getInfo/get all consistently moved to streams/chunks/<name>/; assertSafeEntityId retained on the name before it becomes a path segment; absent dirs read as empty via the pre-existing ENOENT handling (so no-chunk abort streams stay cheap). I checked for other consumers of the on-disk layout — the CLI and web read streams through the world API, so the reset path above is the only miss.
  • Reader teardown hardening: the hoisted streamClosed + unconditional teardown() correctly closes the cancel-during-start() races, including the orphaned-interval case (the re-check sits synchronously before the interval is armed, so there's no interleave window).
  • world-testing regression test asserting invocations === 1 for a 5-step signal-bearing workflow is exactly the right level to pin this — it fails as N+1 without the fix.

Non-blocking notes:

  1. No back-compat for pre-upgrade chunks: existing flat-layout chunk files become invisible to sharded readers. For a dev-oriented world that's probably acceptable, but a run suspended pre-upgrade with an in-flight stream will stall or miss data after upgrading. Worth a sentence in the changeset advising a data-dir reset (or, if cheap, a legacy fallback in listChunkFilesForStream when the sharded dir is absent).
  2. The 2 failing Benchmark Postgres CI lanes are 1h-timeout cancellations, and the same benchmark workflow shows cancelled runs on main itself and unrelated branches in the same window — baseline infra instability, not this PR (same lanes are also merely pending on #2808).

VaguelySerious and others added 3 commits July 7, 2026 16:44
Address #2807 review: the tag-scoped clear() listed only the top-level
streams/chunks directory, which after per-stream sharding holds only
subdirectories — so the .{tag}.bin filter matched nothing and the vitest
plugin's per-tag cleanup silently leaked chunk files across sessions. Iterate
each per-stream directory instead. Also note the legacy flat-layout tradeoff
in the changeset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address #2808 review:
- The claimant loop now force-deletes a claim file that persists but never
  parses (after a few observations) so a corrupt/orphan claim can't block its
  token forever — the releaser correctly leaves such files alone, so nothing
  else reaped them. A live hook's claim is still rebuilt from the event log, so
  reaping a corrupt claim can't steal a token from a live hook.
- hook_created now builds the claim path via hookTokenClaimPath() instead of
  inline, so the layout can't drift.
- Drop the 20-round resume-vs-disposal soak test: both orderings are valid so
  it passed even with the fix reverted (a soak, not a regression guard). The
  deterministic mid-teardown test remains the real guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member Author

(AI) Addressed review feedback + related #2808 feedback (merged `main` in first):

#2807 review

  • Blocking (tagged chunk cleanup): `clear()` now iterates each per-stream subdirectory under `streams/chunks/` to delete `.{tag}.bin` files, instead of listing the now-subdirectory-only top-level dir (which silently no-op'd and leaked chunks across vitest sessions). Regression test added in `tag.test.ts`. (dec64b3)
  • Changeset note: documented the legacy flat-layout tradeoff. (dec64b3)
  • The `cancelAbortReaders` success-path-only leak (vercel bot) was already fixed + covered by a test earlier in this PR.

Related #2808 feedback (addressed here)

  • Unparseable claim can block a token forever: the claimant loop now force-deletes a claim file that persists but never parses (after a few observations), so corrupt/orphan debris is reaped. A live hook's claim is still rebuilt from the event log first, so this can't steal a token from a live hook. New tests cover both the live-hook and orphan-debris cases. (bc1e567)
  • Nit: `hook_created` now uses the `hookTokenClaimPath()` helper instead of building the path inline. (bc1e567)
  • Soak test: dropped the 20-round resume-vs-disposal loop (both orderings are valid, so it passed even with the fix reverted); the deterministic mid-teardown test remains the real guard. (bc1e567)

@karthikscale3 karthikscale3 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.

Re-reviewed after the review-feedback commits. All previous comments are addressed — approving.

Follow-up verification:

  • Tag-cleanup gap (my comment #1): fixed in dec64b3 exactly as suggested — clear() now iterates the per-stream subdirectories and deletes .{tag}.bin files within each (ENOENT-tolerant), with a regression test in tag.test.ts. Verified the code and ran it.
  • Legacy-layout note (my comment #2): changeset now documents the flat-layout tradeoff (old chunks not read back, stale files left in place).
  • Bonus: bc1e567 also picks up the #2808 review observations that apply here post-merge — unparseable-claim reaping in the claimant loop (with the live-hook protection via event-log rebuild, both covered by new tests), hookTokenClaimPath() unification, and dropping the 20-round soak test that passed even with the fix reverted. The reaping's 3-observation threshold plus rebuild-from-log makes stealing a live token effectively impossible; the residual read→delete window is the same accepted TOCTOU class as the rest of the module.
  • Suites on the merged branch: @workflow/world-local 441/441, core runtime/serialization 234/234, and the #2795 inline regression test (1 flow invocation for signal-bearing steps) passes.

CI failures are all pre-existing/infra, not this PR:

  • nextjs-webpack canary + E2E Required Check: the known-flaky lane (failing on unrelated PRs too).
  • E2E Vercel Prod (nextjs-webpack): single webhookWorkflow 120s timeout (149/153 passed, including all 28 AbortController + 9 streaming tests); main concurrently had six Vercel Prod lanes failing on unrelated tests (e.g. instanceMethodStepWorkflow on nuxt), so this is prod-infra instability tonight.
  • Vitest Plugin Tests: the documented pre-existing hook-token-reuse fast-handoff flake from #2779. Since bc1e567 touched the claim loop after the PR body's measurement, I re-measured locally on this branch: 12/12 pass — no regression signal (the changed path only fires on null claim reads; the handoff race reads valid claims).

VaguelySerious and others added 2 commits July 7, 2026 18:18
The "next run can claim the token right after the previous run disposed it"
test raced ~10% because it started the next claimant immediately after
resumeHook, which only enqueues the previous run's continuation — it does not
wait for that run to process the resume, dispose the hook, and complete. The
next run then legitimately observed the previous run still holding the token
and returned a (correct) conflict, failing the assertion.

Per #2778's guarantee ("disposes AND completes"), drive each run to completion
before the next reuses the token. Still catches the regression (a lingering
post-completion claim resolves returnValue with a conflict value and fails the
race), and the reclaim-lingering-claim path stays covered by the storage tests.
Verified 50/50 green (previously ~2/20 failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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.

Re-reviewed after the update — the blocking item is fixed exactly as requested, and the two follow-on commits are solid additions. Approving.

Blocking item resolved: the tag-scoped clear() now iterates per-stream subdirectories, with a regression test that writes tagged chunks into two stream dirs and asserts both are actually emptied by clear() — precisely the guard that would have caught the original miss. The changeset also now documents the legacy flat-layout tradeoff, which covers my earlier back-compat note.

On the two additional commits:

  • Unparseable-claim reaping (bc1e5675e): this closes a real gap — and corrects a statement in my #2808 review, where I said unreadable debris "still gets reaped by the claimant-side force-release": it didn't, since that path needs a parsed claim to evaluate releasability, so a corrupt claim file blocked its token permanently with nothing left to reap it. The reap-then-rebuild design is the right shape: it's safe to delete an unparseable file at the canonical path precisely because writeExclusive publishes atomically (a live claim is always whole JSON) and because the follow-up rebuild is event-log-driven — a live hook's claim gets regenerated and the duplicate now conflicts with the real conflictingRunId, which the new test pins down nicely. One theoretical corner: after 3 vanished-claim observations, the deleteJSON could unlink a fresh foreign claim written in the read→delete window; reaching it requires three consecutive cross-process claim/release interleavings plus a mid-window claim, so it's practically unreachable — but a cheap raw-read-and-reparse immediately before the delete (skip if it now parses) would shrink it to the same irreducible residual as the guarded-release path. Non-blocking.
  • Test de-flake (8611a690a): the reasoning is correct — resumeHook only enqueues the continuation, so the old test asserted a stronger guarantee than #2778 makes ("disposes AND completes"); the observed conflicts were legitimate. The rewritten test still fails on the actual regression (a lingering post-completion claim resolves returnValue with a conflict), and the dropped soak test's justification (passed with the fix reverted → not a guard) is documented in the commit.

Verified locally: world-local 441/441 (including the new sharded-clear and corrupt-claim tests), core 1363/1363, world-testing 11/11.

CI: all 9 benchmark lanes show as failed, but every one is a 60/90-minute hard-timeout cancellation, and the Local lanes were killed during "Install dependencies" — they never executed any PR code. That's runner-infra congestion (consistent with the benchmark-workflow cancellations visible on main and unrelated branches since yesterday), not a regression signal.

One small ask, fine as a fast-follow: the claim-reap fix is user-visible world-local behavior but rides on this PR's chunk-sharding changeset; a one-line @workflow/world-local changeset entry for it would keep the changelog accurate.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2807-to-stable origin/stable
git cherry-pick -S e7e5a0e56d10778554b0ea23d0d66ff9feb66bd9    # -S signs the commit
# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2807-to-stable
gh pr create --base stable --head backport/pr-2807-to-stable \
  --title "Backport #2807: <original PR title>" \
  --body "Manual backport of #2807 (cherry-pick e7e5a0e56d10) to \`stable\`."

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

Labels

None yet

Projects

None yet

3 participants