Stop the tab marker from reaching what the agent reads - #608
Stop the tab marker from reaching what the agent reads#608misterbridge wants to merge 16 commits into
Conversation
The 🐴 prefix exists for the user watching the browser. Headless there is no window and no reader, so the marker only ever reaches the agent, which reads it back as part of the page's title.
The load-event path marks on every navigation, independently of the one that fires when the session changes; the headless skip has to cover both. Both now go through mark_tab_soon(), and the marker program moves into tab_marker.py — derived from the MARKER constant, so a change to the prefix cannot desynchronise the JS from the Python that reads it back.
Nothing told the daemon whether the browser it drives has a window. Browser.getVersion does: Chrome reports HeadlessChrome in its user agent when it runs without one. Detected once, at connect, so every marking decision downstream is a flag read rather than a round trip.
page_info() is the title an agent reads most, and the marker rode along in it: agents have reported the 🐴 as part of the site under test. The prefix describes the harness, so it comes off on the way out — and only a leading marker-plus-space does, since a page is free to use the emoji itself.
The CDP target list is a second channel the marker leaks through, and one where it also makes the driven tab look different from every other tab in the list the agent is choosing from.
`browser-harness status` prints it and agents read it back, so it is the third and last channel the prefix reached the agent through.
list_tabs() hides the about:blank tab the harness opens at startup by its title. The marker is prepended to exactly that tab, so the filter stopped matching and the placeholder showed up in the list the agent chooses from.
Unmarking lived in switch_tab(), on the client side, which does not know whether the browser has a window — the same reason marking had to move. The daemon already sees both sessions when set_session swaps them.
It marked unconditionally, from the side that cannot know whether the browser has a window, so a headless tab got the prefix back the moment the agent switched tabs. set_session already tells the daemon to move it.
is_reusable_blank_page() recognises the harness's own startup placeholder by its title, and the marker is prepended to exactly that tab — so the daemon could attach to the placeholder it had just opened. Its sibling in helpers.py already matches on the stripped title; now the two agree.
Every recorded action captured document.title raw into events.jsonl, and video.py copies it into recording-summary.json — which the make-video skill hands to an agent. The video's own tab labels never carried it; this JSON was the last route the prefix took to a reader it misleads.
The marker was documented nowhere an agent reads, and it now shows up in
one place only: js("document.title") — the example in js()'s own docstring
— disagrees with page_info()["title"]. Say so in Gotchas.
These files told the next agent that page_info().title starts with the harness's marker, or quoted a site's title with the prefix baked in. The read APIs no longer return it, so those notes now mislead.
✅ Skill review passedReviewed 1 file(s) — no findings. |
There was a problem hiding this comment.
All reported issues were addressed across 10 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Browser.getVersion can fail or answer without a user agent. Leaving self.headless false then let a headless session write the marker into the titles the agent reads — the bug this branch closes. The marker is cosmetic, so the safe side is to lose it on a headed tab.
Detection rests on a token in the user agent, over which this project has no control. The caller who launched the browser can state the truth.
The mirror of BH_TAB_MARKER=0, for a browser that reports HeadlessChrome while a human watches it. Document both in SKILL.md, and stop a stray BH_TAB_MARKER in a developer's shell from deciding for the tests.
|
Both points are addressed in the three commits just pushed. Thanks — the first one was a real hole. Fail closed ( On the user-agent token ( End to end, the skip does fire: in a headless session the raw in-page That said, the underlying concern is fair — this rests on one string the project does not control. I looked for a better signal and there is none:
So the detection stays, and |
The 🐴 tab marker is written into
document.title— the same channel the page writes to — and nothing removes it before an agent reads it back. So the marker, which describes the harness, reaches the agent as if it described the site.Why this is worse than noise
The marker is not consistently present. Whether a title carries it is decided by a race between the harness and the page:
switch_tab()once already settledSame URL, two access paths, opposite results:
The marker says nothing about the page — it says how the agent got there. That is exactly what makes it read as data: it correlates with real technical properties (render mode, load speed) without being one.
This has already misled agents, in this repo
Six
domain-skillsfiles mention the marker. Four document the trap correctly. Two took it for a property of the site and built on it:agent-workspace/domain-skills/vercel/vercel.md:135— "Page title prefix is a status signal — 🟢 Vercel = all systems nominal"agent-workspace/domain-skills/atlas/overview.md:70— "The app sets a green-dot emoji prefix on titles… Useful forwait_forconditions"Both are pre-existing and untouched here. The same question was raised from outside the project in NousResearch/hermes-agent#85430: "It can be mistaken for actual website content during QA, so a documented explanation — or a less content-like marker / separate metadata field — would help prevent false findings."
What this changes
No marker at all when the browser is headless. With no window there is no one to read it, so it can only pollute. Detected from
Browser.getVersion's user agent at connection time.Every title a helper hands back is stripped —
page_info(),current_tab(),list_tabs(),connection_status(), and the title the recorder traces intorecording-summary.json. Only a leading marker-plus-space is removed;"Horses 🐴 for sale"and"🐴Emoji-first"survive untouched.Marking moves from
switch_tab()into the daemon, which is what knows whether the browser has a window. The fire-and-forget marking on load events is preserved unchanged — awaiting it costs ~4 s per navigation (#136).SKILL.mdgains the warning that was missing: the marker exists, helper-returned titles are stripped of it, andjs("document.title")therefore disagrees withpage_info()["title"]in a headed session. Three domain-skill files that quoted the old marked titles are corrected.The follow-up, and why it comes second
The marker still vanishes when a page rewrites its own title after load — most modern sites, since an SPA or a hydrating framework sets its title after the load event fires. That is a separate bug and the fix is ready: misterbridge/browser-harness-fork#1, six commits on top of this branch, opened there rather than here so its diff shows those six alone. It moves here once this lands.
The order is not a convenience. Making the marker persistent without the stripping in this PR would turn an intermittent leak into a permanent one: today the marker often disappears on its own, so some reads come back clean by accident; re-applied on every title write, it would reach every single read.
Verified
pytest tests -q→ 130 passed.document.titleis never marked, confirming the skip fires rather than the stripping merely hiding it.Summary by cubic
Stops the 🐴 tab marker from reaching agent-visible titles while keeping it visible to users in headed sessions. Previously the harness wrote the marker into document.title and helpers returned it; now headless sessions never mark, and helper/recorded titles strip a leading "🐴 " prefix.
BH_TAB_MARKER=0/1forces off/on.tab_markermodule with shared marker/JS and unit tests; update SKILL.md and domain skills.Migration notes
Written for commit 6f5e184. Summary will update on new commits.