# v0.10.0 — Your tab's logs, and only your tab's

**2026-08-14**

## TL;DR

`list_network_requests` and `list_console_messages` now return what *your* tab did.
They have advertised a `tab` argument since v0.7.0 and never once read it: the handlers
destructured twelve arguments and `tab` was not among them, so every call returned the
whole browser's buffer while the envelope underneath asserted the reply was about your
page. An agent asking for eight requests from its own tab got eight requests from
somebody else's.

The bulk context bundle had the opposite fault. It did filter, by comparing each entry's
context id to the tab id, which is only ever true for the top-level document. A page's
own iframes, and any window it opened, were dropped. A sign-in popup reported `0
requests` while the popup was making twelve.

Both are fixed by the same new thing: the server now tracks how browsing contexts are
related, and an entry belongs to your tab when it came from that tab, from a frame inside
it, or from a window it opened.

## Why

A browsing context is not a tab. It is the unit the browser files activity under, and a
tab is merely the kind of context that has no parent. Every iframe on a page is its own
context, and a `window.open()` popup is a *sibling* top-level context whose only link to
the page that spawned it is an opener field nobody was reading.

So the two failures are the same misunderstanding pointed in opposite directions. Comparing
ids and calling it a filter under-reports, because most of a page's activity is logged
under ids that are not the tab's. Not comparing at all over-reports, because the buffer
is one browser-wide list shared by every agent connected to it.

Neither is a small matter of noise. Over-reporting hands one agent another agent's
traffic, which is a confidentiality problem in a browser several agents share and a
person watches over VNC. Under-reporting is worse than silence: a bundle that says `0
requests` for a sign-in popup reads as *the click did nothing*, and an agent will
reasonably click again.

## Highlights

| | |
|---|---|
| **The list tools read `tab`** | `list_network_requests` and `list_console_messages` return your tab's entries, its frames' entries, and those of any window it opened. The argument they advertised now does something. |
| **`scope: "all"`** | The old browser-wide view, when you want it. Each row is labelled with the word name of the tab that owns it, so a shared list is finally readable. |
| **Iframes count as yours** | Same-origin, `srcdoc`, and cross-origin out-of-process frames all resolve to the tab containing them. A page's own XHR is no longer invisible because it came from a frame. |
| **Popups count as yours** | A window your tab opened is admitted and marked `↗`. The sign-in flow that reported nothing now reports the seventeen requests it actually made. |
| **Nothing vanishes quietly** | A scoped reply that held rows back says so: `hidden: 75 from other tabs (scope:"all" to see them)`. A short list is never ambiguous between a quiet browser and a busy filter. |
| **Unattributed entries fail closed** | An entry the browser named no context for is withheld under tab scope and counted in that same line. In a shared browser it is as likely to be somebody else's as yours. |
| **`clear_console_messages` is scoped** | It clears your tab by default and reports the count. `scope: "all"` still wipes the shared buffer, but you have to ask for it. With no tab resolved it clears nothing rather than guessing. |

## How it works

A new module keeps a map of every browsing context the browser has: its parent, its
opener, and the OS window it is painted in. It is filled from
`browsingContext.contextCreated` and backfilled once at connect time with
`browsingContext.getTree`, because subscriptions only report the future and the tabs an
agent asks about generally predate the connection.

Resolving an entry then means walking parents to the top-level context that owns it. That
alone fixes iframes. Popups need the second link: a popup's parent is null, so the walk
stops at the popup itself, and only its opener says where it came from. A tab admits an
entry when the entry's root *is* the tab, or when following opener links from that root
arrives at the tab — which also covers a popup that opened a popup.

Tabs opened through the automation API carry no opener, which is what keeps `new_page`
from quietly making every agent's tabs into somebody's popups. No bookkeeping was needed
for that; the browser already distinguishes the two.

Nodes outlive the contexts they describe by five minutes, matched to the console and
network buffer TTLs, so an entry logged by a frame that has since been torn down still
resolves instead of falling out as unattributable.

## Verified

Measured on the dev container against two tabs on different domains, `kettle` on
`example.com` and `candle` on Wikipedia.

| Probe | Before | After |
|---|---|---|
| `list_network_requests` for `kettle` | 8 rows returned, all of them Wikipedia's | 1 row, `example.com`, plus `hidden: 75 from other tabs` |
| `list_console_messages` for `kettle`, marker logged in `candle` | marker returned | nothing returned, 1 hidden; visible under `scope: "all"` labelled `tab: "candle"` |
| `srcdoc` iframe fetch, bundle | 0 requests | 1 request |
| Cross-origin out-of-process iframe fetch, bundle | 0 requests | 1 request |
| Top-level fetch, bundle | 1 request | 1 request, not double-counted through the parent walk |
| `window.open()` popup, bundle | 0 requests | 17 requests, each marked `↗`, `fromPopups: 17` |
| `clear_console_messages` for `kettle` | wiped every tab's buffer | cleared 3, left `candle`'s marker standing |

## Upgrading

Scoped is the default, so a caller that passes `tab` — or lets the server infer it, which
is the normal case — gets less than it used to, and what it gets is its own. That is the
point of the release, and the `hidden:` line makes the difference legible rather than
silent. Anything that genuinely wants the whole browser passes `scope: "all"`.

`get_network_request` is unchanged: a request id is still readable across tabs, matching
the soft tenancy the rest of the server uses — tabs are attributable, not walled off.

Without BiDi there is no context tree, and both readers fall back to the exact id
comparison they used before, so a degraded session is no worse than the old behaviour.
