# v0.7.1 — A borrowed tab is now a refusal, not a warning

## TL;DR
v0.7.0 gave every agent an identity and a tab of its own, then undermined both: an agent that named no tab silently fell back to whoever's tab happened to be focused, and was told so in a warning it was free to ignore. Agents ignored it. Two sessions running side by side drove the same tab into each other's work for minutes before either noticed. Writes on a tab you do not hold are now refused outright, the refusal carries enough of the page that no follow-up call is needed to understand what was nearly clobbered, and tab ownership is legible in the VNC view instead of only in tool output.

## Why
Captured from two real sessions running concurrently against one browser. One agent held tab `ed10e6e6`. A second agent, holding nothing, issued a `navigate_page` with no `tab` argument and received:

> `! no tab given and you hold none; fell back to the focused tab ed10e6e6, which belongs to agt_991c65 - call new_page to get a tab of your own`

The call then went through. So did the next one, and the one after — click, scroll, navigate — each prefaced with the same notice, each landing on someone else's page. The owning agent came back to a tab showing an application it had never opened, concluded that tab IDs must have been reshuffled by a reconnect, closed the tab, and opened a fresh one. Neither agent was wrong about its own actions. Both were working from a mental model the server had quietly falsified.

The failure is not that the agent missed the warning. It is that a warning was the wrong instrument. An agent mid-task carries momentum: a line of advisory text arriving alongside a successful result reads as noise, because the result says the operation worked. Anything the server is not willing to enforce, it should not pretend to police.

## What changed

**Writes on a fallback-resolved tab are refused.** When a tab is resolved by falling back to the focused tab — no `tab` argument, no tab held — any tool that could change what the page shows now returns an error and performs no action. Read-only tools are unaffected, so an agent can still orient itself. The distinction is resolution *source*, not ownership: an explicitly named tab is honoured, a guessed one is not.

**The refusal carries the page with it.** The whole point of the borrowed-tab problem is that the agent does not know what it is standing on, and a bare error invites a probing call that lands on the same tab. Each refusal therefore includes the tab ID and title, the URL, the current owner (or `nobody - a person may be using it`), a structural summary — link count, form-control count — and the first 600 characters of visible text, plus a screenshot:

```
navigate_page refused: no tab given, and you hold none.
  tab    b4bf9e74
  title  thinkpad:gh-md-editor
  url    http://192.168.1.19:3458/?folder=/home/ken/dev/gh-md-editor
  owner  nobody - a person may be using it
  page   0 links, 101 form controls
  text   gh-md-editor /home/ken/dev/gh-md-editor v0.23.0 ...
```

The text summary is not redundant with the screenshot. Some MCP clients — Claude Code among them, verified at the wire level across every hop of a four-layer proxy chain — discard non-text content blocks when a response is flagged as an error, while forwarding the same image without complaint on a successful one. The refusal keeps its error flag, because downgrading a refusal to a success to satisfy one client's rendering would be a protocol lie and would reintroduce exactly the ignorable-warning failure this release exists to remove. The digest is what makes the refusal complete on clients that drop the picture.

**Naming an unowned tab claims it.** Passing an explicit `tab` that nobody holds, on a tool that writes, transfers ownership to the caller. Deliberate use is a claim; there is no reason to make an agent follow every explicit write with a `claim_tab`. Ownership still cannot be taken from another agent — that path returns the existing owner and stops.

**Every non-fallback resolution moves the cursor.** Working on a tab makes it the agent's current tab, so a sequence of calls against one page needs the `tab` argument only once.

**A tab remembers who last changed it.** When a call could have altered what a tab displays, the tab records the agent and the tool. The next caller to touch that tab is told:

> `! agt_15461e ran navigate_page on this tab 4s ago - the page may not be the one you left`

This is the one place a soft signal is still correct: the operation is legitimate, the tab is legitimately held, and the only useful thing to say is that the ground may have shifted.

Which tools leave that mark is narrower than "anything that mutates", and getting it wrong is worse than leaving no mark at all. `new_page` mutates, but the tab resolved for it is whatever was in front — not the tab it goes on to create — so stamping it accused innocent agents of touching pages they never opened, usually a human's. `claim_tab` and `release_tab` move ownership without altering a single pixel of the document. Both are excluded. A breadcrumb the next caller learns to distrust is the same confusion, one level up.

## Ownership is now visible in the browser
Up to now, working out which agent held which tab meant reading tool output. Anyone watching the browser over VNC — including a human sharing it interactively — saw an undifferentiated strip of tabs.

Every managed tab now carries a corner badge with its short tab ID and owning agent, and a generated favicon in the same colour, so the tab strip itself is readable at a glance. Colour is derived per agent and stable for that agent's lifetime; **grey is reserved for tabs nobody has claimed**, which makes a human-opened tab visually distinct from an agent's workspace without anyone having to label it. Marks are repainted after any call that changes ownership or content, so they never go stale.

The badge is injected into the page, so it is excluded from `query_dom` output and from the accessibility snapshot collector — an agent never sees its own furniture.

## Known limitation
The tenancy registry is in memory. A server restart re-mints agent identities and drops every claim; tabs survive, ownership does not. Persistence is deliberately out of scope here — it is a data-lifetime question, not a correctness one, and the refusal behaviour is what was urgent.

## Files changed
- `src/firefox/tab-digest.ts` *(new)* — text page summary attached to refusals
- `src/firefox/tab-marker.ts` *(new)* — VNC badge and favicon, colour per agent, grey for unowned
- `src/index.ts` — refusal gate on fallback-resolved writes, auto-claim on explicit writes, cursor updates, breadcrumb recording, marker repaint
- `src/tenancy.ts` — resolution source tracking, per-tab last-action record, agent colours, envelope lines
- `src/tools/query-dom.ts`, `src/firefox/snapshot/injected/elementCollector.ts` — exclude the badge from page queries

No tool signatures changed. Calls that already named their tab behave exactly as before; the ones that did not were the bug.
