# 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)
