The link command printed “Linked. WhatsApp Web is connected.” Then it printed “Headless did NOT come back ready, so the browser will run minimized instead.” Right after, status read mode minimized, chrome uptime under a minute, state logged_out, daemon state qr_login, last ready 3m ago. A browser that had just reached ready was back on the logged-out screen.

The sequence lives in supervisor._finish_link:

B.write_config(backend="dedicated")
if b._config()["mode"] == "headless":
b.launcher.kill()
b.sleep(1)
b.ensure_up("headless")
if _wait_ready(b, HEADLESS_VERIFY_S): # HEADLESS_VERIFY_S = 90
out("Verified: the linked session also works with no window (headless)...")
return 0
out("Headless did NOT come back ready, so the browser will run minimized instead.")
B.write_config(mode="minimized")
b.launcher.kill()
b.sleep(1)
b.ensure_up("minimized")
b.launcher.minimize()

The reasoning was in the docstring. On 2026-10-01 a live launch was measured: Chrome ignores the start-minimized flag (STARTUPINFO SW_SHOWMINNOACTIVATE), opens a normal window and takes focus, with or without --app. Headless was the only way a restart at logon or after a crash could be quiet, so headless was the default. Whether a linked session survives headless could not be tested before the first scan. So the link command tested it on the real session: kill the browser, relaunch headless, require state ready. On 2026-10-01 that relaunch survived.

The check that cost the scan

On 2026-10-06 the same check failed. The link succeeded in the visible window, the headless relaunch did not come back ready, and the fallback relaunched a second time, minimized. The page ended up logged out. The fallback also persisted mode: minimized in browser_config.json.

Earlier that day the profile had already dropped from ready to logged_out while running headless, with the user agent reporting HeadlessChrome. browser.log shows the start of that run: 09:23:22 chrome launched mode=headless, 09:24:22 state=ready. A status read that evening showed logged_out since 86m ago, chrome uptime 12h 15m, last ready 3h 55m ago.

We sent a session to drive the “Link with phone number” flow by hand over CDP in that headless browser, with the author waiting on the phone’s “Enter code” screen. WhatsApp refused phone-number linking from the headless browser. That route was closed before it produced a code.

So the relaunch cost a QR scan, and getting the scan done was its own problem. The WhatsApp window sat minimized at -31993,-32000, 146x21. desk place was denied because another session held the desk lease. We filed a request, and the output warned that nobody was guaranteed to read it. The QR wait was due to end about 11:11pm.

Reading the page without touching it

We asked a read-only session five questions. The first was whether the page was really on the QR screen, or still loading after the relaunch. It attached over CDP with .scratch/wa_readonly_probe.py, with no clicks and no reload. The probe returned:

"qr_canvas": true,
"qr_dataref": true,
"chat_models": 0,
"chatlist": false,
"body_head": "Download WhatsApp for Windows ... Scan to log in\nLink with phone number instead ..."

The session was logged out and showing the QR page. The cause was not established. The same relaunch survived on 2026-10-01 and did not on 2026-10-06.

Another of the five questions was whether the next successful link would try headless first. The persisted config said minimized, so the mode == "headless" branch would be skipped. The code default was still {"backend": "9222", "mode": "headless"}, though, and a reset or missing browser_config.json would put the kill and relaunch back.

The change

DEFAULT_CONFIG = {"backend": "9222", "mode": "minimized"}

Headless stays in MODES as an explicit opt-in through the CLI whatsapp mode headless. The commit touched 3 files, 49 insertions and 19 deletions, including tests/test_whatsapp_browser.py. The module docstring and the supervisor’s “Why headless is the default” block were rewritten to explain why minimized is now the default.

The trade is written into the docstring: a minimized launch can flash once on a restart, and headless is the only fully silent one.

We relinked by QR and switched the browser to a minimized window by default. It stayed linked overnight. The cause of the original logout is still unknown. With mode defaulting to minimized, a fresh link skips the if b._config()["mode"] == "headless" branch, so b.launcher.kill() never fires on a browser that has just printed ready.