Hmbown/CodeWhale · error

stable session header

Error message

stable session header

What it means

This is a test-side `.expect("stable session header")` panic in the Codewhale TUI client test suite. It fires when the request captured by the wiremock server has no `x-opencode-session` header, or the header value is not valid ASCII/UTF-8 (`to_str()` fails). The test asserts that the opencode gateway client attaches a stable, non-empty session header on every outbound request, so a panic here means header injection regressed or the request was sent by a code path that skips session headers.

Solutions

  1. Inspect the captured request (`request.headers`) and confirm `x-opencode-session` is set by `prepare_outbound_request` for all three wire formats.
  2. Verify the client carries a session id before the first request and reuses it across turns (stability assertion at the same lines).
  3. Check that the session header value is ASCII-printable so `to_str()` succeeds; reject non-ASCII at session-id creation.
  4. Fix in crates/tui/src/client.rs request-preparation code, not in the test, unless the header contract itself changed.

Example fix

// before
let observed_session = header("x-opencode-session").expect("stable session header");
// after (in the client, the actual fix)
headers.insert(
    "x-opencode-session",
    HeaderValue::from_str(&self.session_id).expect("session id is ASCII"),
);
Defensive patterns

Strategy: validation

Validate before calling

// Rust (test-side guard before parsing)
let raw = request.headers.get("x-opencode-session");
assert!(raw.is_some(), "x-opencode-session header must be present");
assert!(raw.unwrap().to_str().is_ok(), "session header must be visible ASCII");

Type guard

fn session_header<'a>(req: &'a RequestRecord) -> Option<&'a str> {
    req.headers.get("x-opencode-session").and_then(|v| v.to_str().ok()).filter(|s| !s.is_empty())
}

Prevention

When it happens

Trigger: Running `cargo test -p codewhale-tui` and reaching `opencode_go_dispatches_all_three_wires_with_gateway_auth_and_stable_session` when `prepare_outbound_request` fails to set `x-opencode-session` (missing session id, header only set on one wire format, or header name typo), or the header value contains non-visible-ASCII characters so `HeaderValue::to_str()` returns None.

Common situations: Refactoring the outbound request builder so session propagation is dropped for the Responses/ChatCompletions/AnthropicMessages wires; changing the header casing/name; introducing a binary or non-ASCII session token that cannot round-trip through `to_str()`.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/0216f5609ef623af. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/client.rs:8078

                        .is_err(),
                    "a request cannot silently change an existing client's protocol"
                );
            } else {
                client
                    .create_message(minimal_zen_request(model))
                    .await
                    .unwrap();
            }
            let requests = server.received_requests().await.unwrap();
            assert_eq!(requests.len(), 1);
            let request = &requests[0];
            let header = |name: &str| {
                request
                    .headers
                    .get(name)
                    .and_then(|value| value.to_str().ok())
            };
            let observed_session = header("x-opencode-session").expect("stable session header");
            assert!(!observed_session.is_empty());
            if let Some(previous) = &session {
                assert_eq!(observed_session, previous);
            }
            session = Some(observed_session.to_string());
            assert!(header("user-agent").is_some_and(|value| value.contains("Codewhale") || value.contains("codewhale")));
            for forbidden in ["openai-beta", "originator", "chatgpt-account-id"] {
                assert!(
                    header(forbidden).is_none(),
                    "gateway requests cannot carry {forbidden}"
                );
            }
            let body: Value = serde_json::from_slice(&request.body).unwrap();
            assert_eq!(body["model"], model);
            if wire == WireFormat::AnthropicMessages {
                assert_eq!(header("x-api-key"), Some("go-test-key"));
                assert_eq!(header("anthropic-version"), Some("2023-06-01"));
                assert!(header("authorization").is_none());

View on GitHub (pinned to 73e0f67d83)