diff --git a/elixir/lib/symphony_elixir/codex/app_server.ex b/elixir/lib/symphony_elixir/codex/app_server.ex index 7da87ce9..9f36bc7a 100644 --- a/elixir/lib/symphony_elixir/codex/app_server.ex +++ b/elixir/lib/symphony_elixir/codex/app_server.ex @@ -422,15 +422,17 @@ defmodule SymphonyElixir.Codex.AppServer do {:error, _reason} -> log_non_json_stream_line(payload_string, "turn stream") - emit_message( - on_message, - :malformed, - %{ - payload: payload_string, - raw: payload_string - }, - metadata_from_message(port, %{raw: payload_string}) - ) + if protocol_message_candidate?(payload_string) do + emit_message( + on_message, + :malformed, + %{ + payload: payload_string, + raw: payload_string + }, + metadata_from_message(port, %{raw: payload_string}) + ) + end receive_loop(port, on_message, timeout_ms, "", tool_executor, auto_approve_requests) end @@ -977,6 +979,13 @@ defmodule SymphonyElixir.Codex.AppServer do end end + defp protocol_message_candidate?(data) do + data + |> to_string() + |> String.trim_leading() + |> String.starts_with?("{") + end + defp issue_context(%{id: issue_id, identifier: identifier}) do "issue_id=#{issue_id} issue_identifier=#{identifier}" end diff --git a/elixir/test/symphony_elixir/app_server_test.exs b/elixir/test/symphony_elixir/app_server_test.exs index b7fab152..d03627f8 100644 --- a/elixir/test/symphony_elixir/app_server_test.exs +++ b/elixir/test/symphony_elixir/app_server_test.exs @@ -1188,17 +1188,94 @@ defmodule SymphonyElixir.AppServerTest do labels: ["backend"] } + test_pid = self() + on_message = fn message -> send(test_pid, {:app_server_message, message}) end + log = capture_log(fn -> - assert {:ok, _result} = AppServer.run(workspace, "Capture stderr log", issue) + assert {:ok, _result} = + AppServer.run(workspace, "Capture stderr log", issue, on_message: on_message) end) + assert_received {:app_server_message, %{event: :turn_completed}} + refute_received {:app_server_message, %{event: :malformed}} assert log =~ "Codex turn stream output: warning: this is stderr noise" after File.rm_rf(test_root) end end + test "app server emits malformed events for JSON-like protocol lines that fail to decode" do + test_root = + Path.join( + System.tmp_dir!(), + "symphony-elixir-app-server-malformed-protocol-#{System.unique_integer([:positive])}" + ) + + try do + workspace_root = Path.join(test_root, "workspaces") + workspace = Path.join(workspace_root, "MT-93") + codex_binary = Path.join(test_root, "fake-codex") + File.mkdir_p!(workspace) + + File.write!(codex_binary, """ + #!/bin/sh + count=0 + while IFS= read -r line; do + count=$((count + 1)) + + case "$count" in + 1) + printf '%s\\n' '{"id":1,"result":{}}' + ;; + 2) + printf '%s\\n' '{"id":2,"result":{"thread":{"id":"thread-93"}}}' + ;; + 3) + printf '%s\\n' '{"id":3,"result":{"turn":{"id":"turn-93"}}}' + ;; + 4) + printf '%s\\n' '{"method":"turn/completed"' + printf '%s\\n' '{"method":"turn/completed"}' + exit 0 + ;; + *) + exit 0 + ;; + esac + done + """) + + File.chmod!(codex_binary, 0o755) + + write_workflow_file!(Workflow.workflow_file_path(), + workspace_root: workspace_root, + codex_command: "#{codex_binary} app-server" + ) + + issue = %Issue{ + id: "issue-malformed-protocol", + identifier: "MT-93", + title: "Malformed protocol frame", + description: "Ensure malformed JSON-like frames are surfaced to the orchestrator", + state: "In Progress", + url: "https://example.org/issues/MT-93", + labels: ["backend"] + } + + test_pid = self() + on_message = fn message -> send(test_pid, {:app_server_message, message}) end + + assert {:ok, _result} = + AppServer.run(workspace, "Capture malformed protocol line", issue, on_message: on_message) + + assert_received {:app_server_message, %{event: :malformed, payload: "{\"method\":\"turn/completed\""}} + assert_received {:app_server_message, %{event: :turn_completed}} + after + File.rm_rf(test_root) + end + end + test "app server launches over ssh for remote workers" do test_root = Path.join(