Skip to content

Commit 6b5de0c

Browse files
committed
Add reason_code to TransportError and normalize payload errors
Updated Codex.TransportError to include a reason_code field for structured error reporting. The Exec module now extracts the error code from incoming payloads, normalizing binary strings to atoms to maintain consistent internal representation. Codex.Error.normalize was updated to ensure this metadata is preserved and surfaced during error handling.
1 parent bd284bb commit 6b5de0c

4 files changed

Lines changed: 40 additions & 8 deletions

File tree

lib/codex/error.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ defmodule Codex.Error do
6060
exit_status: error.exit_status,
6161
stderr: error.stderr,
6262
stderr_truncated?: error.stderr_truncated?,
63-
retryable?: error.retryable?
63+
retryable?: error.retryable?,
64+
reason_code: error.reason_code
6465
}
6566
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
6667
|> Map.new()

lib/codex/runtime/exec.ex

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,21 @@ defmodule Codex.Runtime.Exec do
8989

9090
@spec session_error(CoreEvent.t(), binary(), boolean()) :: {:error, term()} | nil
9191
def session_error(
92-
%CoreEvent{kind: :error, raw: %{exit: %CoreProcessExit{} = exit}},
92+
%CoreEvent{
93+
kind: :error,
94+
raw: %{exit: %CoreProcessExit{} = exit},
95+
payload: %Payload.Error{} = payload
96+
},
9397
stderr,
9498
stderr_truncated?
9599
) do
96100
{:error,
97101
Codex.TransportError.new(exit_code(exit),
98-
message: exit_message(exit),
102+
message: payload.message || exit_message(exit),
99103
stderr: stderr,
100104
stderr_truncated?: stderr_truncated?,
101-
retryable?: retryable_exit?(exit)
105+
retryable?: retryable_exit?(exit),
106+
reason_code: normalize_reason_code(payload.code)
102107
)}
103108
end
104109

@@ -545,6 +550,16 @@ defmodule Codex.Runtime.Exec do
545550
"codex executable exited: #{inspect(reason)}"
546551
end
547552

553+
defp normalize_reason_code(nil), do: nil
554+
defp normalize_reason_code(code) when is_atom(code), do: code
555+
556+
defp normalize_reason_code(code) when is_binary(code) do
557+
code
558+
|> String.downcase()
559+
|> String.replace("-", "_")
560+
|> String.to_atom()
561+
end
562+
548563
defp retryable_exit?(%CoreProcessExit{} = exit),
549564
do: Codex.TransportError.retryable_status?(exit_code(exit))
550565
end

lib/codex/transport_error.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ defmodule Codex.TransportError do
77
whether to attempt automatic retries.
88
"""
99

10-
defexception [:message, :exit_status, :stderr, :stderr_truncated?, :retryable?]
10+
defexception [:message, :exit_status, :stderr, :stderr_truncated?, :retryable?, :reason_code]
1111

1212
@type t :: %__MODULE__{
1313
message: String.t(),
1414
exit_status: integer(),
1515
stderr: String.t() | nil,
1616
stderr_truncated?: boolean(),
17-
retryable?: boolean()
17+
retryable?: boolean(),
18+
reason_code: atom() | nil
1819
}
1920

2021
@doc """
@@ -39,7 +40,8 @@ defmodule Codex.TransportError do
3940
stderr: stderr,
4041
stderr_truncated?: stderr_truncated?,
4142
message: message,
42-
retryable?: retryable?
43+
retryable?: retryable?,
44+
reason_code: Keyword.get(opts, :reason_code)
4345
}
4446
end
4547

test/codex/error_test.exs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Codex.ErrorTest do
22
use ExUnit.Case, async: true
33

4-
alias Codex.{Error, Options, Thread}
4+
alias Codex.{Error, Options, Thread, TransportError}
55
alias Codex.Thread.Options, as: ThreadOptions
66

77
test "non-zero codex exit normalizes into Codex.Error" do
@@ -37,6 +37,20 @@ defmodule Codex.ErrorTest do
3737
assert error.details.codex_error_info == %{"code" => "rate_limit"}
3838
end
3939

40+
test "normalize/1 preserves transport reason codes" do
41+
error =
42+
TransportError.new(127,
43+
message: "Codex CLI not found on remote target ssh-target.example",
44+
stderr: "env: 'codex': No such file or directory",
45+
reason_code: :cli_not_found
46+
)
47+
|> Error.normalize()
48+
49+
assert error.message =~ "Codex CLI not found"
50+
assert error.details.reason_code == :cli_not_found
51+
assert error.details.exit_status == 127
52+
end
53+
4054
defp temp_script(contents) do
4155
path = Path.join(System.tmp_dir!(), "codex_error_#{System.unique_integer([:positive])}")
4256
File.write!(path, contents)

0 commit comments

Comments
 (0)