vercel / vercel/eve

world-local 30s hook timeouts redeliver and re-bill a long model call; process death mid-call does not always replay

Open
#2,425 7 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug core p2
Dominant language
TypeScript
Stars
5.3k
Forks
569
Avg merge
21h 56m
Merged PRs (30d)
531

Description

Summary

Two related world-local / workflow-sdk failures around a single in-flight model call:

  1. The default 30s WORKFLOW_LOCAL_BODY_TIMEOUT_MS / WORKFLOW_LOCAL_HEADERS_TIMEOUT_MS on the world-local Undici hook redelivers a long Responses generation and OpenRouter (or any provider) bills twice for one visible answer.
  2. A model call interrupted by process death does not always replay. Sometimes the stream stops at step.started after restart even though the run is re-enqueued.

Raising the timeouts to 600s narrows the double-bill window. It does not close it. A call that stays open past the timeout still redelivers and still re-bills, because the recovered inline step is not idempotent.

eve 0.44.0. Local eve start / world-local, WORKFLOW_NODE_HTTP unset.

Repro 1 — 30s hook timeout double-bills a long generation

Installed world-local (eve/dist/src/compiled/@workflow/world-local/index.js) resolves:

bodyTimeout: yn(`WORKFLOW_LOCAL_BODY_TIMEOUT_MS`, 3e4)
headersTimeout: yn(`WORKFLOW_LOCAL_HEADERS_TIMEOUT_MS`, 3e4)

Cn() builds new mn.Agent(t) from those values when WORKFLOW_NODE_HTTP is unset. The Node HTTP path reads the same pair.

A Responses stream that stays open past 30s trips the hook fetch. Core then logs:

[world-local] Queue delivery failed at the transport (loop 2), retrying
[workflow-sdk] Re-executing inline steps owned by this queue message — a previous delivery crashed mid-body...

and re-executes the inline step. The provider completes both requests.

Forced both env vars to 30000 on an otherwise identical server. One generation produced two identical request bodies 35.263s apart and two charges:

seq cost
2 $0.031748
3 $0.026858

One visible assistant completion. Two message.received executions.

Setting both to 600000 on the same code, a 59.734s generation made one request and incurred one $0.015660 charge.

That is causal for the knob. It is not a fix for non-idempotent execution: any generation longer than the configured timeout still duplicates.

Tradeoff of 600s: a genuinely stuck hook now stalls ~10 minutes instead of ~30 seconds. Recorded, accepted.

This is distinct from #455 (queue visibility ~300s) and #1930 (immediate fetch failed on every delivery).

Repro 2 — killed in-flight model step does not always replay

Kill the listener PID after allowModelCall / outbound fetch has started and before the provider body completes.

Observed (eve 0.44, local start):

  • Startup: Re-enqueued N active run(s) on startup
  • After >75s the stream still ended at step.started
  • No replacement outbound request, no answer, no terminal failure
  • Repeating with 30s local timeouts still did not recover it

This is inconsistent. Other kills of the same process (different landing relative to the model call) replayed cleanly. Where the kill lands relative to the hook body seems to decide it.

A later follow-up POST on that session returned 202 but still did not produce a new outbound request.

Expected: either replay the interrupted model step, or fail the step so a new turn can run. Today the run can sit at step.started forever from the client's point of view.

What would actually close the double-bill

Idempotent model execution (or a durable request identity that the recovered step reuses instead of issuing a second provider call). Timeout knobs only move the window.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the world-local implementation at eve/dist/src/compiled/@workflow/world-local/index.js, including Cn(), new mn.Agent(t), and the inline-step recovery path described in the report. Reproduce the 30s timeout and process-death cases, then inspect how queue redelivery and startup re-enqueue handle an in-flight model call. Done means interrupted calls recover or fail visibly, and a redelivery does not create a second provider charge.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
backend, distributed-systems
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.