--- type: workflow title: WhatsApp relink and pairing description: End-to-end trace of pairing wh as a WhatsApp linked device and of re-pairing without restarting — F8 twice in the TUI or "Nuovo QR" in the web panel, App::start_relink, WaHandle::relink (logout, stop, session.db deletion or in-place reset, new unpaired client), the new QR on the TUI pairing screen and in the web pairing card, failure behaviour and tests. tags: [whatsapp, pairing, relink, qr-code, session, web-panel, tui] verified: - by: openwiki/0.7.2 at: 2026-10-23T21:26:24.156Z sources: - id: openwiki-source-9fec9d1af408501c9c455161 resource: repo://src/app.rs - id: openwiki-source-ac7a3eaa396cc3aa88c00eef resource: repo://src/web_page.html - id: openwiki-source-7b75006986d8b0337adc6ca0 resource: repo://src/web.rs - id: openwiki-source-7ea5eac45e5030178a6ea05b resource: repo://src/whatsapp.rs generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" } --- # WhatsApp relink and pairing `wh` is a WhatsApp *linked device*: it pairs by QR code and keeps the device credentials in `session.db` (see [Configuration and data](../operations/configuration-and-data.md)). Pairing happens on first start, and can be redone at any time without restarting the process — to link another account, or after the QR codes expired — with **F8 pressed twice** in the TUI or the **"Nuovo QR"** button of the [web control panel](../integrations/web-control-panel.md). ## Flow ``` TUI: F8 -> confirm prompt -> F8 Web: "Nuovo QR" -> confirm() -> POST /api/relink (Esc / other key cancels) (X-Wh-Request: 1) \ / v v App::request_relink App::handle_web_request(Relink) \ / +--------> App::start_relink <--------+ (ignored while relinking) screen = Connecting, qr cleared, status "Cancellazione della sessione in corso...", own_jids_known = false | tokio task: WaHandle::relink 1. take session out of the slot 2. Session::stop: logout, wait <= 10 s, abort translator 3. reset_session_files: delete session.db (+ -wal, -shm) or, if locked, empty its tables in place 4. open_session: new unpaired client on the same events channel | AppEvent::Relinked(result) -> App::finish_relink (status) | new client -> WaEvent::Qr -> Screen::Pairing, qr_code stored | \ TUI pairing screen (half-block QR) GET /api/state: screen "pairing", | qr_svg -> web pairing card phone scans -> WaEvent::Connected -> Screen::Main, own JIDs refreshed ``` ## 1. Requesting a relink **TUI.** F8 works on every screen (connecting, pairing and main), so a stuck or expired pairing can be restarted from the pairing screen itself, whose key help reads `F8: nuovo QR F10: esci` ([Terminal UI](../architecture/terminal-ui.md)). `handle_key` checks F8 right after F10 (`src/app.rs:884-903`). `request_relink` (`src/app.rs:356-369`) cancels a pending chat deletion; the first press sets `pending_relink` and shows `Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla`; the second press calls `start_relink`. Any other key in between clears the flag through `cancel_pending_relink` (`src/app.rs:371-381`) with the status `Nuovo abbinamento annullato`; an Esc used that way does nothing else, other keys are then handled normally. **Web panel.** "Nuovo QR" (`relinkBot`, `src/web_page.html:703-718`) asks `Cancellare la sessione WhatsApp e rifare l'abbinamento? Il dispositivo verrà scollegato dal telefono.` and posts `/api/relink` with the mandatory `X-Wh-Request: 1` header (`src/web.rs:166-174`, `src/web.rs:238-254`). `App::handle_web_request` maps `WebCommand::Relink` straight to `start_relink` — the browser dialog replaces the double F8 ([Application state](../architecture/app-state.md#web-panel-requests)). ## 2. Starting it in `App` `start_relink` (`src/app.rs:320-341`) clears `pending_relink` and returns at once if `relinking` is already set, so repeated requests during a relink are ignored. Otherwise it sets `relinking`, switches to `Screen::Connecting`, drops the stored QR, shows `Cancellazione della sessione in corso...` and resets `own_jids_known`, because the next pairing may be a different account and the [autoreply gate](ai-autoreply.md) must learn its JIDs again. It then spawns a tokio task that awaits `WaHandle::relink` and sends `AppEvent::Relinked` with `Ok(())` or the error text. The web request returns the state snapshot taken right after `start_relink`, i.e. `screen: "connecting"` and no QR; the page shows its pairing card with "Connessione a WhatsApp in corso..." and starts polling every 2 s. ## 3. Replacing the session `WaHandle::relink` (`src/whatsapp.rs:221-237`, see [WhatsApp client](../integrations/whatsapp-client.md#relink)): 1. **Take** the current `Session` out of the shared slot. From now until step 4 succeeds every client operation fails with `WhatsApp session not running`: manual and AI sends end with `Invio fallito: ...`, image downloads fail, `own_jids` is empty. 2. **Stop** it (`Session::stop`, `src/whatsapp.rs:182-198`): `logout` deregisters the device on the phone when connected (best effort) and disconnects; then it waits up to `STOP_TIMEOUT_SECONDS` (10 s) for the client run loop to report it stopped, and aborts the translator task, which holds a reference to the client. 3. **Reset** the session database (`reset_session_files`, `src/whatsapp.rs:314-329`): delete `session.db` and its `-wal` / `-shm` side files. If the deletion fails — on Windows `whatsapp-rust` keeps the SQLite file open until the process exits — `clear_session_tables` (`src/whatsapp.rs:349-367`) opens it with `rusqlite` (5 s busy timeout) and, in one transaction, deletes every row of every table except SQLite internals and `__diesel_schema_migrations`, so the schema stays valid for the next client. If both fail the error reads `cannot delete () nor clear session database `. 4. **Start** a new client on the empty database with `open_session` (`src/whatsapp.rs:369-416`) and put it in the slot. It sends its events to the same `WaEvent` channel as the old one, so the forwarder and `App` need no rewiring. Only `session.db` is reset. `history.db` — chats, messages, contacts, autoreply flags and image descriptions — is untouched, so chats of the previously linked account stay visible until deleted locally. ## 4. Outcome and new QR `finish_relink` (`src/app.rs:343-354`) clears `relinking` and shows `Sessione cancellata: scansiona il nuovo QR` or `Nuovo abbinamento fallito: `. After a failure the handle has no session: the app stays on the connecting screen and the user can press F8 twice again (or restart). The new client emits `WaEvent::Qr` payloads; `handle_wa_event` switches to `Screen::Pairing` and stores the payload (`src/app.rs:406-411`). It is shown in two places: - the TUI pairing screen draws it with half-block characters (`qr_to_text`), or a "terminal too small" message ([Terminal UI](../architecture/terminal-ui.md#qr-code)); - `App::web_state` renders it to SVG with `web::qr_svg` (`src/web.rs:364-379`) and returns it as `qr_svg` with `screen: "pairing"`; the page's pairing card shows it as an image and the WhatsApp linking steps, polling every 2 s so rotated codes appear quickly (`src/web_page.html:596-628`). When the library runs out of QR codes the status shows `pairing QR codes exhausted; request a new QR to pair again` — another relink starts a fresh sequence. Scanning the code produces `WaEvent::Connected`: the app switches to `Screen::Main`, clears the QR, shows `Connesso` and fetches the new account's JIDs; the web page hides the pairing card and returns to 5 s polling. ## Failure and edge cases - A relink requested while another runs is ignored (`relinking`). - If the old client does not stop within 10 s the relink continues anyway; on a still-open database the in-place reset is used. - The startup catch-up runs once per process (`catch_up_done`), so it is not repeated after a relink. - The web panel is unauthenticated: anyone reaching the port can unlink the device and, while pairing, scan the QR to link their own phone (see [Web control panel](../integrations/web-control-panel.md#security-and-operations)). - Never run two copies of `wh` with the same `session.db` (`README.md`). ## Tests - `web.rs`: `POST /api/relink` resolves to the relink route, requires the `X-Wh-Request` header, and `GET /api/relink` is not found; `qr_svg` produces an SVG document (`src/web.rs:422-442`). - `whatsapp.rs`: the ignored live test `relink_deletes_the_session_and_offers_a_new_qr` (`src/whatsapp.rs:1163-1183`) starts a real client, waits for a QR, relinks, waits for the next QR and checks that the device keys in the two payloads differ; run it with `cargo test -- --ignored` (network needed). - The `App` side (F8 confirmation, `start_relink`, `finish_relink`) and the page script have no automated tests. See [Unit tests](../testing/unit-tests.md).