diff --git a/agent/opencode_affinity.py b/agent/opencode_affinity.py index 6df0bf1cc9..321312c455 100644 --- a/agent/opencode_affinity.py +++ b/agent/opencode_affinity.py @@ -18,6 +18,7 @@ matching per-model wire-format decision for the auxiliary client. from __future__ import annotations +import uuid from typing import Any, Optional OPENCODE_SESSION_HEADER = "x-opencode-session" @@ -100,7 +101,13 @@ def opencode_session_headers( ) except Exception: key = str(session_id or "") - return {OPENCODE_SESSION_HEADER: key} if key else {} + if not key: + # Stateless one-shot requests (commit messages, summaries, standalone prompts outside + # a session) lack an ambient conversation or session id. OpenCode Go strictly requires + # x-opencode-session on every request (HTTP 400 MissingSessionID if absent, #105841) + # so generate an ephemeral session id fallback. + key = f"oneshot-{uuid.uuid4().hex[:16]}" + return {OPENCODE_SESSION_HEADER: key} def merge_opencode_session_headers( diff --git a/tests/agent/test_opencode_session_affinity.py b/tests/agent/test_opencode_session_affinity.py index ee7877816f..35ea5cd725 100644 --- a/tests/agent/test_opencode_session_affinity.py +++ b/tests/agent/test_opencode_session_affinity.py @@ -147,3 +147,17 @@ def test_tui_gateway_oneshot_runtime_snapshot_carries_the_session(monkeypatch, o aux.call_llm(task="title_generation", main_runtime=_main_runtime_from_agent(agent), messages=_MSGS) assert captured["extra_headers"]["x-opencode-session"] == "sess-desktop-1" + + +def test_stateless_oneshot_still_sends_an_opencode_session_header(out_of_turn): + """A one-shot with no live session (Desktop commit-message generation from the review panel with + no active chat, standalone aux calls) has no conversation identity at all, yet the relay rejects + header-less requests with 400 MissingSessionID (#105841). It must carry an ephemeral key instead + of nothing; non-OpenCode targets stay untouched.""" + from agent.opencode_affinity import opencode_session_headers + + kwargs = aux._build_call_kwargs("opencode-go", "glm-5", _MSGS, base_url="https://opencode.ai/zen/go/v1") + assert kwargs["extra_headers"]["x-opencode-session"] + + assert opencode_session_headers("opencode-go", None, session_id=None).get("x-opencode-session") + assert opencode_session_headers("openrouter", "https://openrouter.ai/api/v1", session_id=None) == {}