world-local 30s hook timeouts redeliver and re-bill a long model call; process death mid-call does not always replay
Nobody has claimed this yet.
- 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:
- The default 30s
WORKFLOW_LOCAL_BODY_TIMEOUT_MS/WORKFLOW_LOCAL_HEADERS_TIMEOUT_MSon the world-local Undici hook redelivers a long Responses generation and OpenRouter (or any provider) bills twice for one visible answer. - A model call interrupted by process death does not always replay. Sometimes the stream stops at
step.startedafter 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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