# WS Browser Welcome Frame Compatibility Implementation Plan > **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:** Make the ws service path tolerate the real sgBrowser welcome banner (`Welcome! You are client #...`) without weakening general ws protocol validation or changing pipe behavior. **Architecture:** Keep the shared `WsBrowserBackend` strict and implement the compatibility shim only in `ServiceBrowserWsClient`, which is already the real-browser adapter for the ws service path. Add one positive red test for the known welcome frame and one negative red test proving non-matching first text frames still fail as protocol errors, then make the minimal stateful change in `src/service/server.rs` and verify ws + pipe regressions. **Tech Stack:** Rust 2021, tungstenite websocket client/server, existing `WsBrowserBackend`, existing `ServiceBrowserWsClient`, existing Rust unit/integration test suite. --- ## File Structure ### Existing files to modify - Modify: `src/service/server.rs` - Add the one-time per-connection welcome-skip state to `ServiceBrowserWsClient` - Add the minimal helper(s) for detecting and discarding the first known welcome frame - Add focused service-adapter unit tests in the existing `#[cfg(test)]` module - Reuse: `src/browser/ws_backend.rs` - Do not change protocol parsing rules; only verify behavior remains strict for all non-service callers - Reuse: `tests/service_task_flow_test.rs` - Re-run to confirm the ws service path still reaches the browser websocket after the service-side shim - Reuse: `tests/browser_ws_backend_test.rs` - Re-run to prove the shared backend semantics remain unchanged ### Files deliberately not changed - `src/browser/ws_backend.rs` - `src/browser/ws_protocol.rs` - `src/agent/task_runner.rs` - `src/compat/runtime.rs` - `src/compat/orchestration.rs` - `src/compat/workflow_executor.rs` - `src/lib.rs` The design explicitly keeps the welcome-banner workaround out of the shared backend and out of the pipe path. --- ## Task 1: Reproduce the real welcome-frame failure with focused unit tests **Files:** - Modify: `src/service/server.rs` - [ ] **Step 1: Add the positive failing test for the known welcome frame** In the existing `#[cfg(test)] mod tests` inside `src/service/server.rs`, add one focused test next to the current ws adapter tests. Test shape: ```rust #[test] fn future_server_side_ws_native_adapter_skips_initial_known_welcome_frame() { // fake server sends: // 1. "Welcome! You are client #1" // 2. "0" // backend.invoke(Action::Navigate, ...) should succeed } ``` Required assertions: - the fake websocket server accepts one connection - it sends the welcome banner first, then the numeric success status - `WsBrowserBackend.invoke(Action::Navigate, ...)` returns `Ok(CommandOutput { success: true, .. })` - [ ] **Step 2: Run only the positive new test and watch it fail** Run: ```bash cargo test service::server::tests::future_server_side_ws_native_adapter_skips_initial_known_welcome_frame -- --nocapture ``` Expected: FAIL with a protocol error containing `invalid browser status frame: Welcome! You are client #1`. - [ ] **Step 3: Add the negative failing test for arbitrary first text** In the same `#[cfg(test)]` module, add one negative test proving we do **not** silently skip arbitrary first text frames. Test shape: ```rust #[test] fn future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame() { // fake server sends: // 1. "Hello from server" // assert invoke(...) fails as PipeError::Protocol(...) } ``` Required assertions: - the first frame is a non-matching text frame such as `Hello from server` - `invoke(...)` fails - the failure remains a protocol error rather than success or timeout - [ ] **Step 4: Run only the negative new test and verify the current behavior is already strict** Run: ```bash cargo test service::server::tests::future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame -- --nocapture ``` Expected: PASS, proving the current implementation already treats arbitrary first text as a protocol error. Keep that assertion in place before any production change. - [ ] **Step 5: Confirm the TDD gate before implementation** Do not implement production code before both tests exist and the positive test has failed on current behavior. --- ## Task 2: Add the minimal per-connection welcome-skip state in the service adapter **Files:** - Modify: `src/service/server.rs` - [ ] **Step 1: Add one-time per-connection state to `ServiceBrowserWsClient`** Extend `ServiceBrowserWsClient` with one extra state field that tracks whether the initial welcome candidate has already been consumed for the current websocket connection. Allowed shape: ```rust struct ServiceBrowserWsClient { browser_ws_url: String, browser_socket: Mutex>>>, initial_text_frame_checked: Mutex, } ``` or an equally small equivalent. Rules: - state is per connection, not per request - state must survive multiple `invoke(...)` calls while reusing the same socket - do not add broader protocol state machines - [ ] **Step 2: Add a narrow welcome-frame matcher** In `src/service/server.rs`, add one small helper that recognizes only the known banner prefix: ```rust fn is_known_welcome_frame(frame: &str) -> bool { frame.starts_with("Welcome! You are client #") } ``` Rules: - no regex needed - no generic “ignore arbitrary text” logic - keep the matcher local to `src/service/server.rs` - [ ] **Step 3: Update `recv_text_timeout(...)` to skip at most one initial known banner** Modify `impl WsClient for ServiceBrowserWsClient` so that the first text frame received after connection establishment is handled like this: 1. read the next text frame 2. if the initial-frame state is still false: - mark the first-frame check as consumed - if the frame matches `is_known_welcome_frame(...)`, read the next frame and return that next frame instead 3. otherwise, return the frame unchanged Rules: - skip only once per connection - do not loop indefinitely over multiple text frames - do not swallow unknown first text frames - do not change timeout / close / reset / connect-failure behavior - [ ] **Step 4: Reset the one-time state when a fresh socket is created** When `with_socket(...)` establishes a brand-new websocket connection, ensure the one-time banner-check state is reset so a new connection can tolerate its own first welcome frame. - [ ] **Step 5: Add one reconnect regression in the service adapter tests** Add one focused test proving the welcome skip resets on a fresh connection after socket close/reset. Test shape: ```rust #[test] fn future_server_side_ws_native_adapter_skips_welcome_again_after_reconnect() { // first connection closes after use // second fresh connection sends the same welcome banner again // both invocations succeed } ``` Required assertion: - the one-time skip is per connection, not global for the client instance - [ ] **Step 6: Run the positive new test** Run: ```bash cargo test service::server::tests::future_server_side_ws_native_adapter_skips_initial_known_welcome_frame -- --nocapture ``` Expected: PASS. - [ ] **Step 7: Run the negative new test** Run: ```bash cargo test service::server::tests::future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame -- --nocapture ``` Expected: PASS, proving unknown first text is still treated as a protocol error. - [ ] **Step 8: Run the reconnect regression** Run: ```bash cargo test service::server::tests::future_server_side_ws_native_adapter_skips_welcome_again_after_reconnect -- --nocapture ``` Expected: PASS. - [ ] **Step 9: Run the full service adapter unit group** Run: ```bash cargo test service::server::tests -- --nocapture ``` Expected: PASS, including the existing tests for: - status `0` success - connect failure => `PipeError::Protocol("browser websocket connect failed: ...")` - disconnect/reset => `PipeError::PipeClosed` - callback timeout => `PipeError::Timeout` - new known-welcome success path - new unknown-first-frame strictness path - new reconnect reset behavior --- ## Task 3: Verify the shared backend stayed strict and the ws service path still works **Files:** - Reuse: `tests/browser_ws_backend_test.rs` - Reuse: `tests/service_task_flow_test.rs` - Reuse: `src/browser/ws_backend.rs` - [ ] **Step 1: Re-run the shared ws backend tests unchanged** Run: ```bash cargo test --test browser_ws_backend_test -- --nocapture ``` Expected: PASS. This proves `WsBrowserBackend` semantics remain unchanged for its existing deterministic callers. - [ ] **Step 2: Re-run the service task-flow regression** Run: ```bash cargo test --test service_task_flow_test -- --nocapture ``` Expected: PASS, including the auth-regression test that proves the ws service path reaches the browser websocket and no longer emits `invalid hmac seed: session key must not be empty`. - [ ] **Step 3: Re-run the ws-focused mixed verification** Run: ```bash cargo test --test browser_ws_backend_test --test browser_ws_protocol_test --test service_ws_session_test --test service_task_flow_test -- --nocapture ``` Expected: PASS. --- ## Task 4: Re-run the real manual smoke that originally failed **Files:** - Reuse only: no code changes unless a fresh reproducer proves another bug - [ ] **Step 1: Confirm real browser websocket reachability** Run a reachability check for `ws://127.0.0.1:12345` (or the configured `browserWsUrl`) before starting smoke. Expected: reachable. - [ ] **Step 2: Start the real ws service** Run: ```bash cargo run --bin sg_claw -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json" ``` Expected: the service prints: - `sg_claw ready: ...` - the resolved `service_ws_listen_addr` - the configured `browser_ws_url` - [ ] **Step 3: Re-run the original failing manual smoke** Run: ```bash printf '打开知乎热榜并读取页面主区域文本\n' | cargo run --bin sg_claw_client -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json" ``` Expected: - no `invalid browser status frame: Welcome! You are client #1` - browser actions proceed past the first status frame - if the browser later fails for another reason, capture that new reason exactly - [ ] **Step 4: Re-run the old Zhihu export task smoke** Run: ```bash printf '读取知乎热榜数据,并导出 excel 文件\n' | cargo run --bin sg_claw_client -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json" ``` Expected: - no `invalid browser status frame: Welcome! You are client #1` - the task reaches the real browser action path beyond connection banner handling - [ ] **Step 5: Stop and debug if a new real-browser issue appears** If smoke now fails for a different reason, do not piggyback a second fix into this slice without: - capturing the exact new output - writing a new focused spec/plan if the issue is materially different --- ## Verification Checklist ### Service adapter unit tests ```bash cargo test service::server::tests -- --nocapture ``` Expected: all service-side ws adapter tests pass, including the new welcome-frame positive/negative cases and reconnect reset case. ### Shared ws backend + ws service regressions ```bash cargo test --test browser_ws_backend_test --test browser_ws_protocol_test --test service_ws_session_test --test service_task_flow_test -- --nocapture ``` Expected: PASS. ### Real smoke verification - `browserWsUrl` reachable - `sg_claw` starts with real config - `sg_claw_client` no longer fails on `Welcome! You are client #...` - Zhihu minimal read task gets past the first status frame - Zhihu export task gets past the first status frame