codex-ruby 0.1.4 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ba6980e6b87c0c4edff93f2577d49f167bf05264ba84e94077b6014deba03e9
4
- data.tar.gz: 4dac330c5ddefe50cae38009aa706a2782b1673cbefe78122b934b894b92a5bb
3
+ metadata.gz: 8b977d75f6a42ed832175f5f2fe94a51edc5cee832e23de58ef7a154062310e9
4
+ data.tar.gz: 9742a0cd52e621689755eee26f7f41cf199b2f964dced853e41765ae44595a82
5
5
  SHA512:
6
- metadata.gz: 920bf04a7dbc12d6129b761032c61fcf5929fe22b4370fab1f47e07eb46fa7578908c7cb448776ebfa8dbefc5d34c6728172a3bb2804e139363edbb7b7dc0ef0
7
- data.tar.gz: a76ee269289b0e7441f27ca14710b51d411a31505bd8a337973e0f63d2614eaea5f0da2e3c6971381b9d95a74ea7a979458503c0f7eca90bca8e96761e43dc08
6
+ metadata.gz: f7f9f2b48f75d8f07bf09befcd9f5f5d85c420f4757ea11f4d9292f6421e4c7d2d345ac29b414280df2fb05815ab382f996fe2b6efa99568cae19b52c53fa5fb
7
+ data.tar.gz: 6cad8b57516073b897fb6465f3a8409995559cf616928cd3ae9663ffe0afa6afe8315c9ca9e904a9ea9e389727cc2909aeb3264398c0879441d53c5c729103ae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.3 (2026-09-23)
4
+
5
+ - Preserve provider failures when macOS reports a transient permission error for an exiting process group; verify that the group is gone before ignoring cleanup errors.
6
+
7
+ ## 0.2.2 (2026-09-23)
8
+
9
+ - Clear the exposed PID after an already-exited CLI has been reaped, preventing stale process diagnostics.
10
+
11
+ ## 0.2.1 (2026-09-23)
12
+
13
+ - Expose the active CLI PID through `AgentThread#pid` for worker recovery diagnostics.
14
+
15
+ ## 0.2.0 (2026-09-23)
16
+
17
+ - Forward ordered `local_image` inputs through the public thread API; reject unsupported input types and invalid local paths instead of silently discarding them.
18
+ - Add `ignore_user_config`, `ignore_rules`, and `ephemeral` thread options; use the documented `--json` CLI flag.
19
+ - Honor explicit environment replacement without inheriting parent secrets.
20
+ - Terminate and reap CLI process groups on cancellation or consumer errors, with concurrent interruption serialized.
21
+ - Raise on streams that end without a completed turn instead of returning partial answers as success.
22
+
23
+ ## 0.1.5 (2026-06-17)
24
+
25
+ - Include structured Codex CLI error events in non-zero exit errors, so
26
+ API failures such as unsupported model names are visible to callers
27
+
3
28
  ## 0.1.4 (2026-06-16)
4
29
 
5
30
  - Pass `web_search: true` to current Codex CLI versions as the global
@@ -38,7 +38,8 @@ module CodexSDK
38
38
 
39
39
  # Streaming run: yields each event to the block as it arrives.
40
40
  def run_streamed(input, turn_options: TurnOptions.new, &block)
41
- prompt = normalize_input(input)
41
+ prompt, images = normalize_input(input)
42
+ completed = false
42
43
 
43
44
  output_schema_path = nil
44
45
  output_schema_path = write_output_schema(turn_options.output_schema) if turn_options.output_schema
@@ -50,19 +51,27 @@ module CodexSDK
50
51
 
51
52
  @exec.run(
52
53
  prompt,
54
+ images: images,
53
55
  resume_thread_id: @id,
54
56
  output_schema_path: output_schema_path
55
57
  ) do |event|
56
58
  # Capture thread ID from first event
57
59
  @id = event.thread_id if event.is_a?(Events::ThreadStarted)
60
+ completed = true if event.is_a?(Events::TurnCompleted)
58
61
 
59
62
  block.call(event)
60
63
  end
64
+ raise Error, "Codex stream ended without turn completion" unless completed
61
65
  ensure
62
66
  @context_snapshot = @exec&.context_snapshot
63
67
  cleanup_output_schema(output_schema_path)
64
68
  end
65
69
 
70
+ # PID of the running CLI, for external lifecycle diagnostics.
71
+ def pid
72
+ @exec&.pid
73
+ end
74
+
66
75
  # Interrupt the running subprocess.
67
76
  def interrupt
68
77
  @exec&.interrupt
@@ -73,13 +82,32 @@ module CodexSDK
73
82
  def normalize_input(input)
74
83
  case input
75
84
  when String
76
- input
85
+ [input, []]
77
86
  when Array
78
- input.filter_map do |entry|
79
- entry[:text] if entry[:type] == "text"
80
- end.join("\n\n")
87
+ texts = []
88
+ images = []
89
+ input.each do |entry|
90
+ raise ArgumentError, "Unsupported input entry" unless entry.is_a?(Hash)
91
+
92
+ case entry[:type]
93
+ when "text"
94
+ raise ArgumentError, "Text input must be a string" unless entry[:text].is_a?(String)
95
+
96
+ texts << entry[:text]
97
+ when "local_image"
98
+ path = entry[:path]
99
+ unless path.is_a?(String) && path.start_with?(File::SEPARATOR) && File.file?(path) && File.readable?(path)
100
+ raise ArgumentError, "Local image must be an absolute readable file path"
101
+ end
102
+
103
+ images << path
104
+ else
105
+ raise ArgumentError, "Unsupported input type: #{entry[:type].inspect}"
106
+ end
107
+ end
108
+ [texts.join("\n\n"), images]
81
109
  else
82
- input.to_s
110
+ raise ArgumentError, "Unsupported input: expected string or array"
83
111
  end
84
112
  end
85
113
 
@@ -5,7 +5,7 @@ require "json"
5
5
 
6
6
  module CodexSDK
7
7
  # Internal: manages the codex CLI subprocess.
8
- # Spawns `codex exec --experimental-json`, writes prompt to stdin,
8
+ # Spawns `codex exec --json`, writes prompt to stdin,
9
9
  # reads JSONL events from stdout.
10
10
  class Exec
11
11
  SHUTDOWN_TIMEOUT = 10 # seconds to wait after SIGTERM before SIGKILL
@@ -30,8 +30,11 @@ module CodexSDK
30
30
  sessions_root = codex_sessions_root(env)
31
31
  started_at = Time.now
32
32
  @context_snapshot = nil
33
+ diagnostics = []
33
34
 
34
- @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(env, *args)
35
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(env, *args, unsetenv_others: !@options.env.nil?,
36
+ pgroup: true)
37
+ @pid = @wait_thread.pid
35
38
 
36
39
  # Write prompt and close stdin (one-shot, matching TypeScript SDK)
37
40
  @stdin.write(prompt.to_s)
@@ -56,6 +59,7 @@ module CodexSDK
56
59
  end
57
60
 
58
61
  event = Events.parse(data)
62
+ capture_diagnostic(event, diagnostics)
59
63
  block.call(event)
60
64
  end
61
65
 
@@ -64,9 +68,9 @@ module CodexSDK
64
68
 
65
69
  unless status.success?
66
70
  code = status.exitstatus || status.termsig
67
- truncated = stderr_buf.length > 500 ? "#{stderr_buf[0, 497]}..." : stderr_buf
71
+ diagnostic = exit_diagnostic(diagnostics, stderr_buf)
68
72
  raise ExecError.new(
69
- "Codex exited with code #{code}: #{truncated}",
73
+ "Codex exited with code #{code}: #{diagnostic}",
70
74
  exit_code: code,
71
75
  stderr: stderr_buf
72
76
  )
@@ -83,32 +87,72 @@ module CodexSDK
83
87
  # Sends SIGTERM to the subprocess, waits, then SIGKILL if needed.
84
88
  def interrupt
85
89
  @mutex.synchronize do
86
- return unless @wait_thread&.alive?
90
+ return unless @pid
87
91
 
88
92
  begin
89
- Process.kill("TERM", @wait_thread.pid)
93
+ signal_group("TERM")
90
94
  rescue Errno::ESRCH
95
+ @wait_thread.value
96
+ @pid = nil
91
97
  return
92
98
  end
93
99
 
94
100
  # Wait for graceful shutdown
95
- unless wait_for_exit(SHUTDOWN_TIMEOUT)
96
- begin
97
- Process.kill("KILL", @wait_thread.pid)
98
- rescue Errno::ESRCH
99
- # already gone
100
- end
101
+ wait_for_exit(SHUTDOWN_TIMEOUT)
102
+ begin
103
+ # Also stop descendants that outlive the immediate CLI child.
104
+ signal_group("KILL")
105
+ rescue Errno::ESRCH
106
+ # already gone
101
107
  end
108
+ @wait_thread.value
109
+ @pid = nil
102
110
  end
103
111
  end
104
112
 
105
113
  private
106
114
 
115
+ def signal_group(signal)
116
+ Process.kill(signal, -@pid)
117
+ rescue Errno::EPERM
118
+ # macOS can report EPERM while the last group member is exiting.
119
+ # Only treat it as gone when a subsequent group probe confirms ESRCH.
120
+ @wait_thread.join(0.1)
121
+ Process.kill(0, -@pid)
122
+ raise
123
+ end
124
+
125
+ def capture_diagnostic(event, diagnostics)
126
+ message =
127
+ case event
128
+ when Events::TurnFailed
129
+ event.error_message
130
+ when Events::Error
131
+ event.message
132
+ when Events::ItemStarted, Events::ItemUpdated, Events::ItemCompleted
133
+ event.item.message if event.item.is_a?(Items::Error)
134
+ end
135
+ diagnostics << readable_error_message(message) if message.to_s.strip != ""
136
+ end
137
+
138
+ def exit_diagnostic(diagnostics, stderr)
139
+ diagnostic = diagnostics.compact.uniq.join("\n")
140
+ diagnostic = stderr.to_s if diagnostic == ""
141
+ diagnostic.length > 500 ? "#{diagnostic[0, 497]}..." : diagnostic
142
+ end
143
+
144
+ def readable_error_message(message)
145
+ parsed = JSON.parse(message.to_s)
146
+ parsed.dig("error", "message") || parsed["message"] || message.to_s
147
+ rescue JSON::ParserError
148
+ message.to_s
149
+ end
150
+
107
151
  def build_args(resume_thread_id: nil, images: [], output_schema_path: nil)
108
152
  codex_path = @options.codex_path || find_codex_path
109
153
  args = [codex_path]
110
154
  args << "--search" if @thread_options.web_search
111
- args.concat(["exec", "--experimental-json"])
155
+ args.concat(["exec", "--json"])
112
156
 
113
157
  # Global config overrides
114
158
  args.concat(ConfigSerializer.to_flags(@options.config)) if @options.config.any?
@@ -125,6 +169,9 @@ module CodexSDK
125
169
  args.concat(["--cd", to.working_directory]) if to.working_directory
126
170
  args << "--dangerously-bypass-approvals-and-sandbox" if to.dangerously_bypass_approvals_and_sandbox
127
171
  args << "--skip-git-repo-check" if to.skip_git_repo_check
172
+ args << "--ignore-user-config" if to.ignore_user_config
173
+ args << "--ignore-rules" if to.ignore_rules
174
+ args << "--ephemeral" if to.ephemeral
128
175
 
129
176
  to.additional_directories.each { |dir| args.concat(["--add-dir", dir]) }
130
177
 
@@ -199,6 +246,7 @@ module CodexSDK
199
246
  end
200
247
 
201
248
  def cleanup
249
+ interrupt
202
250
  @stdin&.close unless @stdin&.closed?
203
251
  @stdout&.close unless @stdout&.closed?
204
252
  @stderr&.close unless @stderr&.closed?
@@ -31,7 +31,10 @@ module CodexSDK
31
31
  :network_access,
32
32
  :web_search,
33
33
  :additional_directories,
34
- :skip_git_repo_check
34
+ :skip_git_repo_check,
35
+ :ignore_user_config,
36
+ :ignore_rules,
37
+ :ephemeral
35
38
  ) do
36
39
  def initialize(
37
40
  model: nil,
@@ -43,7 +46,10 @@ module CodexSDK
43
46
  network_access: nil,
44
47
  web_search: nil,
45
48
  additional_directories: [],
46
- skip_git_repo_check: false
49
+ skip_git_repo_check: false,
50
+ ignore_user_config: false,
51
+ ignore_rules: false,
52
+ ephemeral: false
47
53
  )
48
54
  super
49
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CodexSDK
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codex-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Kopylov
@@ -51,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  - !ruby/object:Gem::Version
52
52
  version: '0'
53
53
  requirements: []
54
- rubygems_version: 3.7.2
54
+ rubygems_version: 4.0.20
55
55
  specification_version: 4
56
56
  summary: Ruby SDK for the Codex CLI
57
57
  test_files: []