Skip to content

Commit 98775f1

Browse files
kevinw-openaicodex
andcommitted
fix(elixir): ignore codex stream noise in dashboard events
Summary: - only emit malformed Codex events for JSON-like protocol frames that fail to decode - keep logging non-JSON stream output without surfacing it to the orchestrator dashboard - add regression coverage for stderr noise and malformed protocol frames Rationale: - Codex can write diagnostics to stderr during healthy turns, and the merged stdio stream was incorrectly surfacing those lines as malformed JSON events in the Symphony UI - keeping malformed detection for JSON-looking frames preserves signal for actual protocol corruption while removing false positives Tests: - mise exec -- mix format lib/symphony_elixir/codex/app_server.ex test/symphony_elixir/app_server_test.exs - MIX_ENV=test mise exec -- mix run --no-start -e 'Application.put_env(:symphony_elixir, :workflow_file_path, System.fetch_env!("SYMPHONY_TEST_WORKFLOW")); Mix.Task.run("test", ["test/symphony_elixir/app_server_test.exs"])' Co-authored-by: Codex <noreply@openai.com>
1 parent b1863e8 commit 98775f1

File tree

2 files changed

+96
-10
lines changed

2 files changed

+96
-10
lines changed

elixir/lib/symphony_elixir/codex/app_server.ex

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -381,15 +381,17 @@ defmodule SymphonyElixir.Codex.AppServer do
381381
{:error, _reason} ->
382382
log_non_json_stream_line(payload_string, "turn stream")
383383

384-
emit_message(
385-
on_message,
386-
:malformed,
387-
%{
388-
payload: payload_string,
389-
raw: payload_string
390-
},
391-
metadata_from_message(port, %{raw: payload_string})
392-
)
384+
if protocol_message_candidate?(payload_string) do
385+
emit_message(
386+
on_message,
387+
:malformed,
388+
%{
389+
payload: payload_string,
390+
raw: payload_string
391+
},
392+
metadata_from_message(port, %{raw: payload_string})
393+
)
394+
end
393395

394396
receive_loop(port, on_message, timeout_ms, "", tool_executor, auto_approve_requests)
395397
end
@@ -895,6 +897,13 @@ defmodule SymphonyElixir.Codex.AppServer do
895897
end
896898
end
897899

900+
defp protocol_message_candidate?(data) do
901+
data
902+
|> to_string()
903+
|> String.trim_leading()
904+
|> String.starts_with?("{")
905+
end
906+
898907
defp issue_context(%{id: issue_id, identifier: identifier}) do
899908
"issue_id=#{issue_id} issue_identifier=#{identifier}"
900909
end

elixir/test/symphony_elixir/app_server_test.exs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1189,14 +1189,91 @@ defmodule SymphonyElixir.AppServerTest do
11891189
labels: ["backend"]
11901190
}
11911191

1192+
test_pid = self()
1193+
on_message = fn message -> send(test_pid, {:app_server_message, message}) end
1194+
11921195
log =
11931196
capture_log(fn ->
1194-
assert {:ok, _result} = AppServer.run(workspace, "Capture stderr log", issue)
1197+
assert {:ok, _result} =
1198+
AppServer.run(workspace, "Capture stderr log", issue, on_message: on_message)
11951199
end)
11961200

1201+
assert_received {:app_server_message, %{event: :turn_completed}}
1202+
refute_received {:app_server_message, %{event: :malformed}}
11971203
assert log =~ "Codex turn stream output: warning: this is stderr noise"
11981204
after
11991205
File.rm_rf(test_root)
12001206
end
12011207
end
1208+
1209+
test "app server emits malformed events for JSON-like protocol lines that fail to decode" do
1210+
test_root =
1211+
Path.join(
1212+
System.tmp_dir!(),
1213+
"symphony-elixir-app-server-malformed-protocol-#{System.unique_integer([:positive])}"
1214+
)
1215+
1216+
try do
1217+
workspace_root = Path.join(test_root, "workspaces")
1218+
workspace = Path.join(workspace_root, "MT-93")
1219+
codex_binary = Path.join(test_root, "fake-codex")
1220+
File.mkdir_p!(workspace)
1221+
1222+
File.write!(codex_binary, """
1223+
#!/bin/sh
1224+
count=0
1225+
while IFS= read -r line; do
1226+
count=$((count + 1))
1227+
1228+
case "$count" in
1229+
1)
1230+
printf '%s\\n' '{"id":1,"result":{}}'
1231+
;;
1232+
2)
1233+
printf '%s\\n' '{"id":2,"result":{"thread":{"id":"thread-93"}}}'
1234+
;;
1235+
3)
1236+
printf '%s\\n' '{"id":3,"result":{"turn":{"id":"turn-93"}}}'
1237+
;;
1238+
4)
1239+
printf '%s\\n' '{"method":"turn/completed"'
1240+
printf '%s\\n' '{"method":"turn/completed"}'
1241+
exit 0
1242+
;;
1243+
*)
1244+
exit 0
1245+
;;
1246+
esac
1247+
done
1248+
""")
1249+
1250+
File.chmod!(codex_binary, 0o755)
1251+
1252+
write_workflow_file!(Workflow.workflow_file_path(),
1253+
workspace_root: workspace_root,
1254+
codex_command: "#{codex_binary} app-server"
1255+
)
1256+
1257+
issue = %Issue{
1258+
id: "issue-malformed-protocol",
1259+
identifier: "MT-93",
1260+
title: "Malformed protocol frame",
1261+
description: "Ensure malformed JSON-like frames are surfaced to the orchestrator",
1262+
state: "In Progress",
1263+
url: "https://example.org/issues/MT-93",
1264+
labels: ["backend"]
1265+
}
1266+
1267+
test_pid = self()
1268+
on_message = fn message -> send(test_pid, {:app_server_message, message}) end
1269+
1270+
assert {:ok, _result} =
1271+
AppServer.run(workspace, "Capture malformed protocol line", issue, on_message: on_message)
1272+
1273+
assert_received {:app_server_message, %{event: :malformed, payload: "{\"method\":\"turn/completed\""}}
1274+
assert_received {:app_server_message, %{event: :turn_completed}}
1275+
after
1276+
File.rm_rf(test_root)
1277+
end
1278+
end
12021279
end

0 commit comments

Comments
 (0)