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>
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
- Add the one-time per-connection welcome-skip state to
- 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.rssrc/browser/ws_protocol.rssrc/agent/task_runner.rssrc/compat/runtime.rssrc/compat/orchestration.rssrc/compat/workflow_executor.rssrc/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, ...)returnsOk(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:
- read the next text frame
- 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
- 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
0success - 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
browserWsUrlreachablesg_clawstarts with real configsg_claw_clientno longer fails onWelcome! You are client #...- Zhihu minimal read task gets past the first status frame
- Zhihu export task gets past the first status frame