# Helper Page Lifecycle Fix v2 — Same-Connection Close + Open > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Prevent orphaned helper pages across process restarts by closing existing ones before opening new ones, all on the same WebSocket connection. **Architecture:** In `bootstrap_helper_page`, after registering with the browser WS, send `sgHideBrowerserClosePage` (best-effort, silently ignored if no page exists), then send `sgHideBrowerserOpenPage`. Change `use_hidden_domain` to `true`. **Tech Stack:** Rust, tungstenite, SuperRPA browser WS protocol --- ### Task 1: Add close-before-open in bootstrap_helper_page **Files:** - Modify: `src/browser/callback_host.rs:345-374` (bootstrap_helper_page function) - [ ] **Step 1: Add close command before open command in bootstrap_helper_page** Replace the current `bootstrap_helper_page` function. After `recv_bootstrap_prelude`, send the close command first, then the open command: ```rust fn bootstrap_helper_page( browser_ws_url: &str, request_url: &str, helper_url: &str, use_hidden_domain: bool, ) -> Result<(), PipeError> { let (mut websocket, _) = connect(browser_ws_url) .map_err(|err| PipeError::Protocol(format!("browser websocket connect failed: {err}")))?; configure_bootstrap_socket(&mut websocket)?; websocket .send(Message::Text( r#"{"type":"register","role":"web"}"#.to_string().into(), )) .map_err(|err| PipeError::Protocol(format!("browser websocket register failed: {err}")))?; let _ = recv_bootstrap_prelude(&mut websocket); // Close any orphaned helper page from a previous process run. // Best-effort: if no page exists, the browser silently ignores this. let (open_action, close_action) = if use_hidden_domain { ("sgHideBrowerserOpenPage", "sgHideBrowerserClosePage") } else { ("sgBrowerserOpenPage", "sgBrowserClosePage") }; let close_payload = json!([request_url, close_action, helper_url]).to_string(); let _ = websocket.send(Message::Text(close_payload.into())); let payload = json!([ request_url, open_action, helper_url, ]) .to_string(); websocket .send(Message::Text(payload.into())) .map_err(|err| PipeError::Protocol(format!("helper bootstrap send failed: {err}")))?; Ok(()) } ``` Key changes from current code: - After `recv_bootstrap_prelude`, add the close command (best-effort, ignore errors) - Compute both `open_action` and `close_action` from `use_hidden_domain` flag - Send close first, then open on the same WebSocket connection - [ ] **Step 2: Change `use_hidden_domain` to `true` in server.rs** In `src/service/server.rs`, at the `start_with_browser_ws_url` call, change `false` to `true`: ```rust match LiveBrowserCallbackHost::start_with_browser_ws_url( browser_ws_url, &bootstrap_url, Duration::from_secs(15), BROWSER_RESPONSE_TIMEOUT, true, // use_hidden_domain: hidden domain for invisible helper ) { ``` - [ ] **Step 3: Build** Run: `cargo build 2>&1` Expected: 0 errors. - [ ] **Step 4: Run callback_host tests** Run: `cargo test --lib -- callback_host 2>&1` Expected: 12 tests pass (including `live_callback_host_sends_bootstrap_open_page_command` which still checks for `sgBrowerserOpenPage` because the test passes `false`, and `live_callback_host_hidden_domain_sends_hide_open_page_command` which passes `true`). Note: The test passes `false` for `use_hidden_domain`, so the close command will use `sgBrowserClosePage`. The test's fake WebSocket server will receive both the close and open frames. The test only checks that `sgBrowerserOpenPage` is present, which is still true. - [ ] **Step 5: Commit** ```bash git add src/browser/callback_host.rs src/service/server.rs git commit -m "fix(callback_host): close orphaned helper page before opening new one on same WS" ``` --- ### Task 2: Full verification **Files:** None (verification only) - [ ] **Step 1: Full test suite** Run: `cargo test 2>&1` Expected: All tests pass except pre-existing `lineloss_period_resolver_prompts_for_missing_period` failure. - [ ] **Step 2: Verify key behavioral changes** Manually confirm: 1. `bootstrap_helper_page` sends close command before open command (both on same WS connection) 2. `use_hidden_domain` is `true` in `server.rs` — helper page opens in hidden domain 3. `Drop for LiveBrowserCallbackHost` remains simple (shutdown only, no close attempt) 4. `cached_host` is still in `mod.rs` outer loop (process-internal deduplication)