From 78b86440fc6f3ae7c46b25edb4597e66b1f43548 Mon Sep 17 00:00:00 2001 From: Bastian Venegas Date: Sun, 29 Mar 2026 13:59:55 -0300 Subject: [PATCH] fix: consolidate duplicate ProviderSession behaviour + fix multi-turn provider corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. Remove duplicate Harness.ProviderSession behaviour — all session modules now use only Harness.Providers.ProviderBehaviour, eliminating conflicting callback warnings. Added missing @impl annotations and stop/1 to MockSession. 2. Fix toLegacyProvider in web store to recognize "cursor" and "opencode" — previously both were silently mapped to "codex", corrupting the provider on second turn and causing "cannot switch provider" errors. Also hardcodes experimentalApi: true in Codex initialize (was conditional on providerOptions), fixing collaborationMode rejection on turn/start. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/harness/lib/harness/provider_session.ex | 27 ------------------- .../lib/harness/providers/claude_session.ex | 2 +- .../lib/harness/providers/codex_session.ex | 16 +++-------- .../lib/harness/providers/cursor_session.ex | 2 +- .../lib/harness/providers/mock_session.ex | 13 ++++++++- .../lib/harness/providers/opencode_session.ex | 2 +- apps/web/src/store.ts | 7 ++++- 7 files changed, 25 insertions(+), 44 deletions(-) delete mode 100644 apps/harness/lib/harness/provider_session.ex diff --git a/apps/harness/lib/harness/provider_session.ex b/apps/harness/lib/harness/provider_session.ex deleted file mode 100644 index 6a5ecee747f9..000000000000 --- a/apps/harness/lib/harness/provider_session.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Harness.ProviderSession do - @moduledoc """ - Behaviour implemented by provider-backed session processes. - - `Harness.SessionManager` dispatches to this surface regardless of whether the - provider uses a persistent stdio process, a turn-scoped CLI, or an HTTP/SSE - bridge. - """ - - @type request_id :: term() - @type decision :: term() - @type answers :: term() - @type params :: map() - @type thread_id :: String.t() - @type turn_id :: term() - @type snapshot :: term() - - @callback start_link(keyword() | map()) :: GenServer.on_start() - @callback wait_for_ready(GenServer.server(), timeout()) :: :ok | {:error, term()} - @callback send_turn(GenServer.server(), params()) :: term() - @callback interrupt_turn(GenServer.server(), thread_id(), turn_id() | nil) :: term() - @callback respond_to_approval(GenServer.server(), request_id(), decision()) :: term() - @callback respond_to_user_input(GenServer.server(), request_id(), answers()) :: term() - @callback read_thread(GenServer.server(), thread_id()) :: {:ok, snapshot()} | {:error, term()} - @callback rollback_thread(GenServer.server(), thread_id(), non_neg_integer()) :: - {:ok, snapshot()} | {:error, term()} -end diff --git a/apps/harness/lib/harness/providers/claude_session.ex b/apps/harness/lib/harness/providers/claude_session.ex index ca5fe7141e45..409e28c99bd7 100644 --- a/apps/harness/lib/harness/providers/claude_session.ex +++ b/apps/harness/lib/harness/providers/claude_session.ex @@ -17,7 +17,6 @@ defmodule Harness.Providers.ClaudeSession do @behaviour Harness.Providers.ProviderBehaviour use GenServer, restart: :temporary - @behaviour Harness.ProviderSession alias Harness.Event @@ -88,6 +87,7 @@ defmodule Harness.Providers.ClaudeSession do GenServer.stop(pid, :normal) end + @impl Harness.Providers.ProviderBehaviour def wait_for_ready(_pid, _timeout \\ 30_000) do # Unlike CodexSession which starts a persistent process in init, # ClaudeSession spawns the CLI process lazily on each send_turn. diff --git a/apps/harness/lib/harness/providers/codex_session.ex b/apps/harness/lib/harness/providers/codex_session.ex index 1c58508f18af..45bc0bf0b2e2 100644 --- a/apps/harness/lib/harness/providers/codex_session.ex +++ b/apps/harness/lib/harness/providers/codex_session.ex @@ -17,7 +17,6 @@ defmodule Harness.Providers.CodexSession do @behaviour Harness.Providers.ProviderBehaviour use GenServer, restart: :temporary - @behaviour Harness.ProviderSession alias Harness.JsonRpc alias Harness.Event @@ -151,6 +150,7 @@ defmodule Harness.Providers.CodexSession do GenServer.stop(pid, :normal) end + @impl Harness.Providers.ProviderBehaviour def wait_for_ready(pid, timeout \\ 30_000) do GenServer.call(pid, :wait_for_ready, timeout) end @@ -197,22 +197,14 @@ defmodule Harness.Providers.CodexSession do def handle_info(:initialize, state) do {id, state} = next_request_id(state) - codex_opts = get_in(state.params, ["providerOptions", "codex"]) || %{} - experimental_api = Map.get(codex_opts, "experimentalApi", false) - - capabilities = - if experimental_api do - %{"experimentalApi" => true} - else - %{} - end - initialize_params = %{ "clientInfo" => %{ "name" => "t3-harness", "version" => "0.1.0" }, - "capabilities" => capabilities + "capabilities" => %{ + "experimentalApi" => true + } } state = send_rpc_request(state, id, "initialize", initialize_params) diff --git a/apps/harness/lib/harness/providers/cursor_session.ex b/apps/harness/lib/harness/providers/cursor_session.ex index 4c59e4fe5fea..7b9c835a551f 100644 --- a/apps/harness/lib/harness/providers/cursor_session.ex +++ b/apps/harness/lib/harness/providers/cursor_session.ex @@ -11,7 +11,6 @@ defmodule Harness.Providers.CursorSession do @behaviour Harness.Providers.ProviderBehaviour use GenServer, restart: :temporary - @behaviour Harness.ProviderSession alias Harness.Event @@ -82,6 +81,7 @@ defmodule Harness.Providers.CursorSession do GenServer.stop(pid, :normal) end + @impl Harness.Providers.ProviderBehaviour def wait_for_ready(_pid, _timeout \\ 30_000) do # Cursor is ready immediately — spawns process on send_turn, not in init. :ok diff --git a/apps/harness/lib/harness/providers/mock_session.ex b/apps/harness/lib/harness/providers/mock_session.ex index 9de8d31a1b42..442d1926e5ac 100644 --- a/apps/harness/lib/harness/providers/mock_session.ex +++ b/apps/harness/lib/harness/providers/mock_session.ex @@ -11,8 +11,8 @@ defmodule Harness.Providers.MockSession do - deltaSizeKb: size of each delta in KB (default: 1) - delayMs: delay between deltas in ms (default: 10) """ + @behaviour Harness.Providers.ProviderBehaviour use GenServer, restart: :temporary - @behaviour Harness.ProviderSession alias Harness.JsonRpc alias Harness.Event @@ -36,6 +36,7 @@ defmodule Harness.Providers.MockSession do @request_timeout 30_000 + @impl Harness.Providers.ProviderBehaviour def start_link(opts) do thread_id = Map.fetch!(opts, :thread_id) @@ -44,17 +45,27 @@ defmodule Harness.Providers.MockSession do ) end + @impl Harness.Providers.ProviderBehaviour def send_turn(pid, params), do: GenServer.call(pid, {:send_turn, params}, 60_000) + @impl Harness.Providers.ProviderBehaviour def interrupt_turn(pid, _tid, _turn_id), do: GenServer.call(pid, :interrupt_turn) + @impl Harness.Providers.ProviderBehaviour def respond_to_approval(_, _, _), do: :ok + @impl Harness.Providers.ProviderBehaviour def respond_to_user_input(_, _, _), do: :ok + @impl Harness.Providers.ProviderBehaviour def read_thread(_, _), do: {:ok, %{}} + @impl Harness.Providers.ProviderBehaviour def rollback_thread(_, _, _), do: {:ok, %{}} + @impl Harness.Providers.ProviderBehaviour def wait_for_ready(pid, timeout \\ 30_000) do GenServer.call(pid, :wait_for_ready, timeout) end + @impl Harness.Providers.ProviderBehaviour + def stop(pid), do: GenServer.stop(pid) + @impl true def init(opts) do state = %__MODULE__{ diff --git a/apps/harness/lib/harness/providers/opencode_session.ex b/apps/harness/lib/harness/providers/opencode_session.ex index a277ed49d640..46cf165f5e97 100644 --- a/apps/harness/lib/harness/providers/opencode_session.ex +++ b/apps/harness/lib/harness/providers/opencode_session.ex @@ -38,7 +38,6 @@ defmodule Harness.Providers.OpenCodeSession do @behaviour Harness.Providers.ProviderBehaviour use GenServer, restart: :temporary - @behaviour Harness.ProviderSession alias Harness.Event @@ -118,6 +117,7 @@ defmodule Harness.Providers.OpenCodeSession do GenServer.stop(pid, :normal) end + @impl Harness.Providers.ProviderBehaviour def wait_for_ready(pid, timeout \\ 30_000) do GenServer.call(pid, :wait_for_ready, timeout) end diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index 4590b2886dac..2e9c3b18f89c 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -193,7 +193,12 @@ function toLegacySessionStatus( } function toLegacyProvider(providerName: string | null): ProviderKind { - if (providerName === "codex" || providerName === "claudeAgent") { + if ( + providerName === "codex" || + providerName === "claudeAgent" || + providerName === "cursor" || + providerName === "opencode" + ) { return providerName; } return "codex";