SIGN IN SIGN UP

fix(byoa): put a deadline on the daemon's short HTTP calls (#40)

`runtimeBest` / `runtimeGet` / `api()` and the heartbeat were bare `fetch`
calls with no signal. That is fine for a server that refuses — the throw is
already handled — but not for one that accepts the connection and then never
answers. A `fetch` that never settles cannot be caught: `try/catch` only sees
throws.

`runtimeBest('/status')` is the first thing a turn awaits after its
`turn START` log, BEFORE the engine spawn. So one non-answering request parks
the turn permanently: `finally { this.busy = false }` never runs, the agent
stays `busy` forever, later wakes collapse to `turn busy — coalescing`, and
the heartbeat interval stops going out until the server marks the computer
offline. One stalled socket takes down every agent on the machine, while the
process stays alive and `--status` still reports it as running.

Observed on a real machine: all 6 agents logged `turn START … spawning
claude` and then produced nothing for 10+ minutes — zero engine children,
~0.5s cumulative CPU, no error, `sample` showing the main thread parked in
`uv__io_poll`. Meanwhile `curl` against the same endpoint answered in 0.76s
and the daemon's exact engine invocation, run by hand in the same agent
workspace, finished in 15s. Neither the server nor the engine was slow; only
the daemon's own pooled HTTP path was stuck. With the deadline in place the
same machine and workload ran all 6 turns to `exit 0`.

Adds `HTTP_TIMEOUT_MS` (default 20s, `CUMORA_HTTP_TIMEOUT_MS` to override) and
applies it to those calls. `api()` keeps a caller-supplied `init.signal` when
one is passed. Deliberately NOT applied to the wake-stream — an intentionally
long-lived SSE connection with its own backoff — or the npm version check.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
B
BillyKing committed
f8d0b970f783245e7b96cc7e71c6c747fec35d5c
Parent: f3537de
Committed by GitHub <noreply@github.com> on 8/20/2026, 6:48:53 AM