Agent-First HTTP v0.13.2

by Agent-First Kit Contributors

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.