Agent-First HTTP v0.14.0: A Tab Someone Is Coming To
A host container sat at high CPU with nothing left to do, because every takeover fetch parked a tab for a human who was never sent — and a browser tuned so live fetches never throttle extends that same courtesy to pages nobody is watching. Tabs now have owners: a tab stays open only while a takeover is pending, and the host reclaims whatever leaks past that.
The report was a container, days old, burning CPU with no work arriving. Its browser held a row of tabs nobody remembered opening — challenge pages still polling, single-page apps still ticking their timers, every one of them running at full speed for an audience of zero.
The full speed was our own doing, and it was correct. The host launches its browser with background throttling disabled, because a live fetch cannot have its readiness checks and screenshots distorted by a browser that decides an unfocused tab deserves four timer ticks a second. That flag is right for every tab a fetch is driving. It is expensive for every tab a fetch forgot.
So the real question was why tabs were being forgotten.
Intent is not ownership
--takeover prepares a tab for a human: open it, navigate it, and if a login
wall or a captcha appears, keep it open and mint a URL a person can use to
clear it. The keeping was implemented as intent — a flag set before the fetch
ran, honored no matter how the fetch ended.
Three endings leaked. A warmed profile that sailed past the wall returned the content directly — the good path, the one the skill file tells agents to prefer — and left the tab open for a human who would never be sent, because no wall meant no URL was ever minted. A fetch that errored kept the tab for the same nobody. And the loop’s own happy ending leaked at the far side: the human clears the wall, the agent re-fetches the tab, gets its content, and walks away — nothing in the flow said the tab’s job was done.
Each leaked tab was one takeover. A host that served a few dozen over some days had a browser full of them, every one unthrottled.
The rule now is ownership: a tab that stays open must have someone coming to
it. A takeover fetch keeps its tab only when a wall was actually detected —
the same condition under which a handoff URL exists to give out. A clean
result closes the tab it opened. Error paths close what they created. The
post-takeover re-fetch that comes back clean closes the tab, completing the
loop. And tab_id on a fetch result now names a tab that is still open, or is
absent — a dead id an agent might re-use is not a fact worth reporting.
The signal that lied
Drivers closing what they open is hygiene, not a guarantee. A driver killed
mid-fetch closes nothing. So the host — the only party that sees every target
— grew a reaper: leftover pages are closed once the host has sat idle past
--tab-idle-timeout-s (fifteen minutes by default, 0 to disable), with a
tab_reaped event naming each one.
The first design asked the browser which tabs were idle: a target with no CDP
session attached has no driver, surely. The integration test refuted it on the
first run. Every page target came back attached: true — including the one
the test had deliberately abandoned — because the host’s own launcher holds an
attachment to every page it manages. On a chromium-family host that flag
describes the launcher, not the drivers, and a reaper keyed to it would have
reaped nothing, forever, while its unit tests passed.
The host already owns a signal that cannot lie this way. Every driver’s CDP traffic flows through the host’s own proxy, so a count of open proxy connections answers “is anyone using this browser” without asking the browser at all. A human at the real display never opens a CDP connection — but they hold a takeover credential, which the host also minted. Either one resets every tab’s idle clock, so activity grants the whole browser a fresh timeout; their joint absence, sustained for the full window, is what idle means.
Two details keep the reaper polite. It only ever closes http(s) pages —
blank tabs and browser-internal pages cost nothing and are nobody’s leak. And
it never closes the last window, because a headful browser exits with it; a
sweep that would empty the browser opens a blank tab first.
What this changes for a long-lived --tab
An agent could previously park state in an open tab indefinitely. That was
never a promise, and it is now explicitly not one: cookies and logins belong
to the profile, which persists on disk and survives every one of these
closures. A tab left alone for fifteen minutes with no takeover pending
belongs to nobody, and the host treats it that way. An agent that finds its
tab gone should read tab_reaped in the host’s event stream as the host
reclaiming an unowned tab — and re-fetch through the profile, which is where
the state it wanted has been all along.