Files
claw/docs/superpowers/plans/2026-04-03-ws-browser-welcome-frame-plan.md
木炎 bdf8e12246 feat: align browser callback runtime and export flows
Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:44:53 +08:00

12 KiB

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:

#[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:

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:

#[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:

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:

struct ServiceBrowserWsClient {
    browser_ws_url: String,
    browser_socket: Mutex<Option<WebSocket<MaybeTlsStream<TcpStream>>>>,
    initial_text_frame_checked: Mutex<bool>,
}

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:

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:

#[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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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

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

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