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.
Summary
relayflowdfailsrun.getunder 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: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-testsstep runsnpx 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-testsis 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=4and 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:
run.getbefore 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.protocol_errorsent me looking at the journal and the socket before I looked at load.Acceptance
run.get.Environment
CLI 2.0.22,
relayflowdfrom@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.