Skip to content

relayflowd run.get times out under CPU load and kills the run: a flow's own test suite can destroy 30 steps of journaled work #522

Description

@khaliqgant

Summary

relayflowd fails run.get under CPU load and the CLI kills the run for it. Two runs of the same 67-step flow died this way, 28 and 30 steps deep, hours of agent work discarded:

FAILED [protocol_error] relayflowd could not complete the run request:
journal client: run.get timed out after 30000ms

The daemon was not resource-starved. At the time of failure it held 11 open file descriptors against a soft limit of 1,048,576, and 25 MB RSS. Journals were small — the largest run journal is 1.8 MB. It was CPU-starved.

By a step the flow itself launched.

What causes it

The flow's unit-tests step runs npx vitest run, which schedules 194 test files across all 8 cores. For the ~30s that takes, the daemon cannot get enough CPU to answer a read within its fixed 30-second budget, and the run dies.

Both failures landed in that window. Run g's evidence file for unit-tests is stamped 18:02:49; the timeout follows immediately.

This is a bad shape: a flow that runs a normal test suite can kill itself, and the failure is reported as a protocol error, which points the author at the daemon rather than at load.

Why the workaround is not the answer

I capped the flow at --maxWorkers=4 and it stopped happening. That works, and it is the wrong place for the fix — it means every flow that runs a build, a test suite, or anything else CPU-hungry has to know to throttle itself to protect the orchestrator. Most authors will discover this the way I did, by losing a long run twice.

What to change

Any of these, roughly in order of value:

  • Retry run.get before declaring the run dead. A read that times out under load is not evidence the run is unrecoverable. The CLI already knows how to resume; a transient read failure should not be terminal.
  • Make the budget adaptive, or raise it materially. 30 seconds is generous when the machine is idle and far too short when it is not. Back off and retry rather than failing closed on the first miss.
  • Distinguish "the daemon is unreachable" from "the daemon did not answer in time." The current message reads as a protocol fault; it is a scheduling one. protocol_error sent me looking at the journal and the socket before I looked at load.
  • Consider whether the run should die at all. The work was complete and journaled. A read timeout lost 30 steps of state that was already durable on disk — the run could have been parked for resume instead.

Acceptance

  • A flow whose deterministic step saturates the machine completes rather than dying on run.get.
  • A read timeout is retried, and the resulting failure (if any) names load or unreachability specifically.
  • A run that cannot be read is parked, not failed, when its journal is intact.

Environment

CLI 2.0.22, relayflowd from @relayflows/runtime-darwin-arm64, macOS arm64, 8 cores. Nine daemons running on the box from unrelated projects, all with live data dirs; the affected one had 8h25m uptime.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggarden-readyScoped and ready for an agent to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions