Repository navigation
Releases: w-partners/Wbrowser
Release list
v0.32.2
The tab cap holds on a busy machine
Agents are held to 8 tabs; older ones are closed as new ones open. Reported: 107 agent tabs
open at once, until the browser connection and then the raw-CDP fallback stopped answering.
Two holes:
- The cap only counted tabs that answered "are you one of ours?" within 600 ms. On a heavily
loaded machine none answered, so it counted none and closed none. Tabs the engine opened
itself are now counted from its own record, whether or not they answer. - The raw-CDP fallback opens tabs too, and had no cap at all. It now keeps the same record
and the same limit. Tabs you opened yourself are never counted or closed, on either path.
wb shot says why there is no screenshot
When a capture failed, the engine reported the reason, and wb shot threw it away and
printed only "Screenshot is empty". It now prints the reason (for example a timeout). The
MCP browser_screenshot tool passes it on too.
Restarting the engine no longer erases its log
wb up truncated the engine log at every start, so the restart that follows a failure
destroyed the record of that failure. The log is now appended to, with a line marking each
start, and rotated once at 5 MB.
v0.32.1
Profile names on Linux and macOS
The profile folder name was cut from the chrome://version path on \ only. On Linux
and macOS that path uses /, so wb windows, /session and /logins showed the whole
path where a name belonged, and an account hint naming the folder (Default,
Profile 3) could never match. Both separators are handled now, in one place.
A pointer to a file that does not exist
The note beside the engine's 127.0.0.1 bind told readers to put gate.js in front of
it for outside access. No such file has ever shipped. The note now says what is
actually needed: your own authenticating proxy in front, with the bind left as it is.
v0.32.0 — a tab's owner is not recorded only where the page can erase it
A tab's owner is no longer recorded only in a place the page can erase
The title tag ([1-2] my-agent …) is how wb close --agent and the raw-CDP lane find an agent's
own tabs. But the title belongs to the page. An SPA rewrites it whenever it likes — X does, for
the unread count — and a MutationObserver puts the tag back. Anything that replaces the document
takes that observer with it, and from then on the tab cannot be identified at all.
Reported 2026-10-03: a tab read [1-?] influencer-whitegun (1) WhiteGun on X… right after opening
and plain (1) WhiteGun on X… minutes later. wb close --agent influencer-whitegun then answered
"no open tabs", and five tabs were left open because none could be told apart from a person's.
The raw-CDP lane refused to attach for the same reason, advising restarts that could not help.
The stamp has always also set window.__wbrowserAgent, which survives title rewrites. That is now
used as a second route:
wb close --agent <name>scans titles first (one HTTP call for every tab at once); if that
finds nothing it asks each page directly, and says so — "title tags were gone (the page rewrote
them); identified 1 tab(s) by asking the page instead".- The raw-CDP lane does the same before refusing to attach.
- Only exact answers are claimed. A page that does not answer is left alone — closing a tab we
could not identify is the accident this exists to prevent, so silence is never consent.
Measured end to end: with the title tag stripped and the observer disconnected (the reported state),
wb read reached the tab and wb close --agent closed exactly it, leaving an untagged tab on the
same URL untouched.
🔵 A 3-minute watch found no natural decay — tag and observer were alive at every 20s sample. The
loss is event-driven, not time-driven, so this fix deliberately does not depend on identifying the
event: whenever the title tag is missing, ask the page.
v0.31.3 — an agent can find its own tabs on the fallback
On the raw-CDP fallback, an agent can find its own tabs again
wb close --agent <name> matches on the [1-2] <agent> title tag. The fallback's createTab
never applied one, so tabs an agent opened on that path were invisible to it: wb close answered
"no open tabs" and closed 0 while the tabs sat open on screen. Reported 2026-10-03 — an agent
left five tabs behind rather than risk closing a person's.
The tag script now lives at module level and both lanes use that one source, so they cannot
drift into tagging different things. Same failure shape as 0.31.2's dead-tab cleanup, which lived
only in rawcdp.js and therefore ran only when the engine was already degraded: behaviour that
differs by code path, where the untested path is the broken one.
Two details that were wrong on the first attempt and are now pinned by tests:
- It stamps after the page settles. Stamping right after
createTabreportedstampedwhile
the title stayed bare — the tab isabout:blankor mid-navigation then, and the navigation
replaces the document, taking the title with it. - It reads the title back before claiming success. The first version pushed
stamped
unconditionally and was wrong on the very first run. When the tag does not take you now get
stampWarningsaying so, and what it costs:wb close --agent <name>will not find that tab.
Verified end to end on the fallback: done:["newtab","stamped"], title
[1-?] stamp-probe2 Example Domain, then wb close --agent stamp-probe2 → Closed 1 tabs,
leaving an untagged tab on the same URL untouched.
v0.31.2 — a dead tab is not a dead socket
A dead tab is not a dead socket, and restarting cannot fix one
wb go https://x.com/home timed out at 90s. The engine said the browser socket was half-dead,
reconnected (no effect), then advised restarting the engine, then restarting Chrome. None of
those remove a dead tab, which is what this actually was.
Measured: that x.com tab had been open for hours. Its websocket connected in 27 ms and
Runtime.evaluate 1 never returned — while /json/version and every other tab answered in
single-digit milliseconds. Chrome was healthy. The browser socket was healthy. One renderer was
gone. Closing that tab made x.com open instantly.
So on a read timeout the engine now asks this tab before blaming the connection:
"readError": "read: this tab's renderer had stopped responding — Chrome and the engine are
both fine (Chrome answered raw CDP instantly; so did the other tabs). Closed the dead tab.
Run the command again and it will open a fresh one.
🔴 Do NOT restart the engine or Chrome for this — neither one would have removed the dead tab."
The probe is a fresh websocket to that one target plus a trivial evaluate, bounded at 2.5s, and
it only runs on a path that has already spent 30s. Unmeasurable returns null, not false — a tab
we could not measure is never closed.
Three agents spent hours on this, and one filed a request to switch the engine to raw CDP, which
would not have helped either: raw CDP failed on that tab exactly the same way. The automatic
dead-tab cleanup shipped in 0.13.9 lived only in rawcdp.js, so it ran only when the engine was
already on the raw-CDP fallback — never on the healthy playwright path where this happened.
v0.31.1 — the tab list must not need the attach that is broken
The tab list no longer needs the attach that may be broken
GET /tabs started with await connect(), so when playwright could not attach it returned
502 — telling a remote caller to restart the engine, which cannot fix a version mismatch. The
tab list is what someone reads to find out what happened, including when the attach is the
thing that broke, so it now falls back to Chrome's own /json/list.
The fallback is degraded on purpose and says so: via: "rawcdp", and drivenBy comes back
null rather than claiming nobody is driving a tab — that mapping lives in the engine, not in
Chrome.
An error that cannot be acted on no longer pretends it can
The engine's generic advice is "restart the engine". When playwright simply cannot attach to
this Chrome version, no restart helps. Reads that fail that way now carry:
"restartWontHelp": true,
"cause": "the engine could not attach to Chrome (playwright ↔ Chrome version mismatch …)",
"whatWorks": "reads that go through Chrome directly (status, autoplay, tabs) still work …"
A wrong instruction is worse than silence, because it gets followed.
Which reads need the attach is now stated up front
capabilities and the MCP tool descriptions both carry needsAttach, from the same table. A
caller learns that windows and logins depend on playwright before one of them fails,
instead of inferring "the browser is broken" from a 502 on two reads out of five.
Fixed: a local file tab was listed as null/C:/Users/...
URL.origin is the literal string "null" for file:, data: and blob:, so stripping the
query string left a path with null glued to the front. A reader cannot tell that from a broken
list. safeUrl now builds from the protocol when there is no real origin, and test/safeurl.test.js
pins both halves — the query string must still never survive, because this string is logged.
v0.31.0 — say Chrome is running when Chrome is running
wb status and /health no longer tell you to start a browser that is already running
If the engine could not attach to Chrome, /health said "The browser is not running — start it
with node launch.js" — while Chrome was up and holding 29 tabs. Measured today: playwright
1.63 bundles Chrome 153 and times out attaching to Chrome 154, and /json/list answered in 13 ms
the whole time. So the one sentence on screen sent people to launch a second browser, and the real
cause was never named.
/health now asks Chrome directly before concluding anything, and reports two separate facts:
"browser": false, ← we could not attach
"chrome": true, ← but Chrome itself is there
"openTabs": 29,
"hint": "Chrome IS running (29 tabs) — do not start another one. The engine cannot attach
to it. Try \"wb down && wb up\" first; if that does not help, set
WBROWSER_FORCE_RAWCDP=1 to drive Chrome over raw CDP, which does not depend on
playwright matching Chrome's version."
Collapsing those two facts into one field was what made the engine unable to say "it is there but
I cannot reach it" — the state it was actually in.
A control surface for a browser you cannot drive from it
New starian.js (port 7982): read-only HTTP + MCP over the tailnet, so another machine can see
what this browser is doing. It cannot drive it. type, click, goto and anything touching
credentials are deliberately absent — the one action is engine-restart, which requires
confirm:true, restarts only the engine, and never touches Chrome (it may be your window).
GET /api/starian/read/{tabs,status,windows,logins,autoplay}— tab URLs have their query
strings stripped, because tokens travel in query strings.POST /api/starian/mcp— JSON-RPC 2.0.tools/listis derived from the same capability table
the REST routes use, so the two cannot drift apart.- Every call, including every refusal, is appended to
~/.local/state/wbrowser/starian-audit.logwith who made it. - tailnet (100.64.0.0/10) and loopback only. The engine itself stays on 127.0.0.1.
health answers in 0.03 s instead of 8.9 s
It was waiting on the engine, which waits on Chrome: 38.5 s under load here. A liveness probe that
outlasts its caller's patience gets reported to the user as "site is down". Now a TCP connect
decides alive-or-dead, the engine's own answer is refreshed in the background, and an aged answer
is labelled with its age rather than served as current. What was never measured says so instead
of guessing.
Fixed: three tests failed for a day while the suite reported OK
test/fallback_recovery.test.js uses node:test, which on node 22 prints # fail 3 and exits
0. A loop that judged by exit code called it OK. The failures were real: the raw-CDP pin added in
0.27.1 reads process.env on its first line, and the test's vm sandbox had no process.
scripts/test.shis now the way to run the suite. It reads each file's summary, not just its
status, so a runner that exits 0 with failures cannot hide them.- The pin's contract is now tested: when pinned, recovery must decline without probing —
each attach attempt leaves another utility world inside Chrome, so probing makes the original
problem worse.
v0.29.1 — write the snapshot where the accident happens
Same reporter, same day, measured on their own machine: v0.29.0's tab recovery did not work where the accident had happened.
The snapshot is now written by launch.js, not only the engine
v0.29.0 wrote the open-tab list from the engine's /health. The machine that lost its tabs runs Chrome without the engine — so tabs-*.json never existed there, and the next restart would have lost tabs just as silently.
A recovery aid that only exists where the accident does not happen is no aid at all. launch.js now takes its own snapshot on every run, so an engine-less host accumulates a record too.
"No record yet" is no longer the same silence as "nothing missing"
With no snapshot file, v0.29.0 printed nothing — identical to a clean restart where every tab came back. On the machine that had just lost tabs. It now says so, and says a record is being started.
🔴 Order mattered, and was wrong first. Taking the snapshot before reporting made the warning impossible to observe: the fresh write satisfied the very check meant to flag its absence. Measured by deleting the file — with the old order the warning never printed. reportMissingTabs() now runs before snapshotTabs() on both paths, and a structural test fails if that is ever reversed (verified by reversing it).
The healthy autoplay case prints one line
With only the failure path printing, "checked and fine" and "never checked" looked the same from outside — both a bare ALREADY_UP:
autoplay ✅ allowed (pages can speak without a click)
🔵 Three releases running, the useful findings came from someone applying the previous one and reporting exactly what they saw. The pattern is the same each time: the tool was correct and said nothing, and silence read as success.
v0.29.0 — say what went missing
Three follow-ups from someone applying v0.28.0 on another machine. Each is a place where the tool was right about the facts and silent about what mattered.
launch.js no longer says only "ALREADY_UP"
Told to run node launch.js to pick up the new autoplay flag, it printed ALREADY_UP and exited. Chrome reads flags only at startup, so nothing applied — and "it said already up" reads as success. The only reason the user noticed was that they went looking; they had to stop Chrome by hand first.
ALREADY_UP now measures whether the running browser actually allows audio, and when it does not, says so plus how to restart and how to verify. It still never stops Chrome itself — that closes tabs belonging to other people.
Tabs that do not come back are now named
Restarting Chrome is sometimes the only way to apply a flag, and the tabs do not return on their own. That same restart left 1 of 2 tabs; the second survived only because the user had written its URL down beforehand.
The engine now snapshots open tabs (throttled, piggybacked on /health, best-effort), and launch.js prints any that did not come back — on both the fresh-launch and ALREADY_UP paths, because staying quiet about a lost tab is the same silence.
🔵 1 tab(s) open before the last shutdown are not back:
STARIAN CANVAS https://…/?board=wgolf-primary
Reopen the ones you still want: wb go <url>
🔵 It prints rather than reopens: a tab closed deliberately before a restart should stay closed, and only a person knows which those were. Blank tabs, chrome://, devtools and our own CDP endpoints are skipped; URLs are deduplicated and never truncated.
wb status reports a Chrome started without the flag
A Chrome missing --autoplay-policy makes every page that speaks go silent after a reload, quietly — the page renders, nothing errors, the sound is simply absent. The flag can drift back on any restart that bypasses launch.js. wb status is where people look when "it worked yesterday", so the check belongs there.
🔵 All three were found by someone else applying the previous release and reporting precisely what they hit — including the parts that were their own doing. None would have surfaced from this side.
v0.28.0 — a page can speak again after a reload
Audio no longer waits for a click
Chrome blocks autoplay until someone interacts with the page, and a reload resets that. So any page that talks — a voice assistant, an alert chime, a read-aloud UI — went silent after every refresh until a human clicked.
Reported from a voice canvas that lost its audio on all 10 of that day's reloads: audioBlocked() was true immediately after Page.reload, and the only way back was injecting a synthetic keypress over CDP.
An agent cannot click on the user's behalf, so from the agent's side that silence was both permanent and invisible — the page renders fine and reports no error.
launch.js now passes --autoplay-policy=no-user-gesture-required.
Measured, one flag apart on the same Chrome 154:
| Chrome | AudioContext | |
|---|---|---|
| without the flag | suspended → suspended |
🔴 blocked |
| with the flag | running → running |
✅ allowed |
🔵 Safe here specifically because this is a dedicated automation profile you open deliberately, not your everyday browser. The flag removes the gesture requirement and grants nothing else.
scripts/check-autoplay.js
A flag in the source is not a flag in the running process — Chrome only reads it at startup, so an already-running browser keeps the old behaviour after an update.
node scripts/check-autoplay.js # this machine
node scripts/check-autoplay.js http://host:9222 # another oneIt probes a fresh tab (an existing tab may already hold a user gesture and would report a false pass) and exits 0 allowed / 1 blocked / 2 could-not-measure.
🔴 Restart Chrome to pick this up. Upgrading the package is not enough — check with the script rather than assuming.