Skip to content

fix: report a duplicate run that this manager does not execute - #1

Open
klaidliadon wants to merge 2 commits into
masterfrom
fix/await-terminal-run
Open

klaidliadon wants to merge 2 commits into
masterfrom
fix/await-terminal-run

Conversation

@klaidliadon

@klaidliadon klaidliadon commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Goal

A caller that starts a run with an idempotency key it already used gets the run's real outcome from Await, even when its await budget is shorter than one poll interval. The manager also stops collecting waiters for runs it never executes.

Problem it solves

  • A finished run reads as running. Start with a duplicate key hands back the existing run. Await then waits for an in-process signal that never comes, because this manager is not executing the run, and only checks the database on a one-second ticker. With a budget under a second, the deadline fires first and Await returns StatusRunning for a run that is already Succeeded or Stuck. A caller that maps Stuck to an error ends up reporting a parked run as in progress.
  • Waiters leak. Start and StartTx register a waiter per run, and only the manager that finishes the run removes it. Every run this manager does not finish leaves its entry in the map for the life of the process: a duplicate start of a terminal run, a launch skipped because the local semaphore is full or a drain started, a failed fast-path claim, an abandoned run, or a StartTx that is never awaited.

What changes

  • Await checks the run once before it starts polling. A terminal run returns at once, whatever the budget. A run that is still going keeps the existing signal-or-poll behavior.
  • Each Await call owns its waiter. It registers a channel on entry and removes it on return, so the map holds only callers that are waiting right now. signal sends to every channel registered for the run. It runs after the terminal status is persisted, so an Await that registers too late to receive it reads the status from the database instead.
  • The launch-once guard moves to the WorkflowRun handle. Start launches through its handle, and a StartTx handle launches on its first Await, as before. Start no longer registers anything.

- await reads the run once before its first poll, so a budget shorter
  than awaitPollInterval still sees a run that is already terminal
- a missed fast-path claim drops the waiter, which signal would never
  reach and which otherwise leaked on every duplicate start

Claude-Session: https://claude.ai/code/session_017Jq12mN4YqPnySKYcACYmQ
- Await subscribes its own channel and removes it on return, so no exit
  path of the run (full semaphore, drain, claim error, abandon) can leave
  an entry behind
- the launch-once guard moves onto the WorkflowRun handle; Start no longer
  registers a waiter
- runLocal goes back to a plain no-op on a missed claim

Claude-Session: https://claude.ai/code/session_01SZexpnMmnzp5BG2YwZyQ17
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.

1 participant