Skip to content

[core] Complete a due wait even when its run is already terminal - #3749

Open
pranaygp wants to merge 1 commit into
mainfrom
wait-completed-terminal
Open

[core] Complete a due wait even when its run is already terminal#3749
pranaygp wants to merge 1 commit into
mainfrom
wait-completed-terminal

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Verification

wait_completed was prevented from being posted when the run is terminal, at both layers:

  1. Runtime — every path in the workflow queue handler that discovers a terminal run (run_started rejected with RunExpiredError/EntityConflictError, run_started answering with a terminal run, the lazy-hook preload's terminal scan, and the node replay loop's hasRecordedTerminalRunEvent check) returned and acked before the elapsed-wait pass.
  2. World — a terminal transition reaps the run's waits (deleteAllWaitsForRun / DELETE FROM waits / releaseRunResources, enforced by the sim's run.resources-released invariant), so wait_completed against a reaped wait failed with WorkflowWorldError: Wait "…" not found. Reproduced directly against world-local before touching anything.

So a wait whose run finished first stayed open in the event log forever. Everything that reads the log — replay, inspect, the dashboard timeline, openHookAndWaitState — saw a run that terminated while still sleeping.

Change

@workflow/core — new runtime/due-waits.ts. A queue message created for a wait now always marks the wait completed when it is due, regardless of run status. On a terminal run that is all the delivery does — no replay, no handleSuspension — and the message is acked only after the write settles, so a transient World failure redelivers rather than dropping the completion. Waits that are not yet due are left to their own continuations. A World that still refuses the write is logged and acked, not nacked: redelivery cannot change that verdict, and burning every delivery on it would end in MAX_DELIVERIES_EXCEEDED.

@workflow/world-local, @workflow/world-postgres, world-sim — accept wait_completed for a wait the terminal transition reaped. The event is recorded, the entity is not resurrected (the reap invariant still holds). Dedup moves to whatever survived the reap: the .completed claim under .locks/ in world-local, the event log in world-postgres and world-sim. Without a matching wait_created the write is still rejected — that is a completion for a wait that never existed.

Tests

  • packages/core/src/runtime/due-waits.test.ts — due/not-due/already-completed selection, resumeAt passthrough, log loading, the conflict / unrecordable / retryable-rethrow outcomes, legacy write shape.
  • packages/core/src/runtime/wait-completion-terminal.test.ts — drives the real queue handler against a fake World for all four terminal-discovery paths; asserts the wait_completed is written, nothing is replayed or re-queued, redelivery does not double-complete, an unrecordable completion still acks, and a transient failure does not ack. 6 of its 8 cases fail without the runtime change.
  • World suites: reaped-wait acceptance, duplicate rejection, never-created rejection, and the unchanged live-run rejection in world-local; the equivalents in world-postgres (against a real Postgres container) and world-sim.

packages/core 2171 pass, world-sim 74 pass, world-postgres 142 pass, world-local 546 pass (its 4 failures are pre-existing chmod-based permission tests that also fail on main in this sandbox).

🤖 Generated with Claude Code

A pending wait parks a delayed continuation message on the workflow queue;
when it fires, the delivery's job is to record the wait's `wait_completed`.
If the run finished first — a `Promise.race([sleep('1h'), hook])` whose hook
won, a cancellation, a failure — every path that discovers the terminal state
returned and acked without writing it, so the wait stayed open in the log
forever: replay, `inspect`, and the dashboard timeline all read a run that
terminated while still sleeping.

The completion is now unconditional on run status. On a terminal run it is all
the delivery does — no replay, no suspension dispatch — and the message is
acked only after the write settles, so a transient World failure redelivers
instead of dropping the completion. Not-yet-due waits are left to their own
continuations.

The Worlds refused the write too: a terminal transition reaps the run's waits
(`run.resources-released`), and `wait_completed` against a reaped wait was
`Wait "…" not found`. world-local, world-postgres, and world-sim now accept it
and record the event without resurrecting the entity, deduplicating against
the surviving `.completed` claim (local) or the event log (postgres, sim).
Older backends that still refuse are logged and acked rather than nacked —
redelivery cannot change that verdict.

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

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
@pranaygp
pranaygp requested review from a team, fantix and msullivan as code owners August 22, 2026 17:04
Copilot AI lite review requested due to automatic review settings August 22, 2026 17:04
@changeset-bot

changeset-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fde1165

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

This PR includes changesets to release 18 packages
Name Type
@workflow/core Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow 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

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.

@vercel

vercel Bot commented Aug 22, 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, v0 Aug 22, 2026 5:07pm
example-nextjs-workflow-webpack Ready Ready Preview, v0 Aug 22, 2026 5:07pm
example-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-astro-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-express-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-fastify-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-hono-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-nestjs-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-nitro-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-nuxt-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-python-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-sveltekit-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-tanstack-start-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workbench-vite-workflow Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workflow-docs Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workflow-swc-playground Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workflow-tarballs Ready Ready Preview, v0 Aug 22, 2026 5:07pm
workflow-web Ready Ready Preview, v0 Aug 22, 2026 5:07pm

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.

2 participants