Agent-First HTTP v0.13.2
A dozen fetches against a healthy host were refused before a single request went out, each one blaming a container that had been running the whole time — because a liveness probe gave a rendering browser 750 milliseconds to answer, and the caller treated losing that race as proof the host was gone.
Twelve fetches went out. Twelve came back refused, none of them having reached the network:
no --endpoint-url or AFHTTP_ENDPOINT_URL was given, so afhttp looked for the
default local host, and default local container `afhttp-host` at
ws://127.0.0.1:9222 is not ready: cdp_timeout: Target.getTargets timed out.
Start one with `afhttp container install` …
The container had been running for a day. Its browser was working. Sent one at a
time, the same URLs returned 200. The message named a cause it had not checked
and pointed at a repair for a machine that needed none.
What the probe was actually measuring
/health answers a question every caller needs: is the backend still there. It
answers it by opening a websocket to the browser and asking for
Target.getTargets — a cheap call, so a cheap budget looked reasonable. Both
steps shared 750 milliseconds.
That budget measures something else. A browser rendering several heavy JavaScript pages at once is not gone; it is busy, and it parks a CDP reply for seconds without anything being wrong. The probe could not tell those apart, and neither could anyone reading its answer.
Reproducing it took one loop. Under three concurrent renders, afhttp health
reported the host unreachable while a fetch issued in the same second returned
200. The browser was answering requests it was supposedly too dead to serve.
The probe now times its connect and its CDP call separately, with budgets sized
for a browser under load rather than an idle one. The asymmetry is the reason: a
probe that answers late costs one /health call, and a probe that answers wrong
costs the request that trusted it.
Discovery was never entitled to that verdict
Widening the budget alone would have moved the threshold, not removed it. The deeper mistake was upstream.
When no --endpoint-url is given, afhttp finds the standard local container and
verifies it. Some of that is discovery’s business: the container is running, its
token is readable, its version matches. One check was not. A degraded reading
means the browser missed a probe, and discovery turned that into a refusal —
non-retryable, so the caller’s --retry never applied, and carrying a diagnosis
it had no evidence for.
The tell was that a caller who names the host explicitly has never had this
gate. --endpoint-url sends the request and lets it fail with whatever actually
broke. Two ways of reaching the same host, two different verdicts, and the one
with more ceremony was the one that failed on a working machine.
Only starting blocks a request now — the host’s own statement that it has no
backend yet, which is a fact rather than a reading. Everything else goes out.
The recovery line stops naming container install for a host that just
answered.
Container install had the same shape one layer down: the first unhappy /health
reading failed the whole install, where a backend still opening its first window
just needed the deadline that was already there.
Pinning the promise, not the code
The regression test drives a stand-in CDP backend that accepts the socket
immediately and answers late. It asserts the host reports ok.
That test earns its place by failing. Set the budget back to 750 milliseconds and it goes red; that is how we know it pins the behaviour rather than the implementation that happens to produce it. A test that passes against the bug it was written for is worse than no test, because it reads like coverage.
The other half was ours to document
Not everything here was a defect. A host is one browser, and every fetch sent to it shares that browser’s memory and CPU. Twelve heavy pages at once was never going to be faster than twelve in sequence, and past the first few they start timing out together — which reads as the site refusing rather than the browser running out of room.
The skill file now says so, next to the advice to raise --timeout-ms instead
of adding concurrency. The bug made a bad workload look like a broken tool; the
tool is fixed, and the workload was still worth naming.