Class: CodexSDK::Exec

Inherits:
Object
  • Object
show all
Defined in:
lib/codex_sdk/exec.rb

Overview

Internal: manages the codex CLI subprocess. Spawns ‘codex exec –experimental-json`, writes prompt to stdin, reads JSONL events from stdout.

Constant Summary collapse

SHUTDOWN_TIMEOUT =

seconds to wait after SIGTERM before SIGKILL

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options, thread_options: ThreadOptions.new) ⇒ Exec

Returns a new instance of Exec.



15
16
17
18
19
20
21
22
23
# File 'lib/codex_sdk/exec.rb', line 15

def initialize(options, thread_options: ThreadOptions.new)
  @options = options
  @thread_options = thread_options
  @stdin = nil
  @stdout = nil
  @stderr = nil
  @wait_thread = nil
  @mutex = Mutex.new
end

Instance Attribute Details

#context_snapshotObject (readonly)

Returns the value of attribute context_snapshot.



13
14
15
# File 'lib/codex_sdk/exec.rb', line 13

def context_snapshot
  @context_snapshot
end

#pidObject (readonly)

Returns the value of attribute pid.



13
14
15
# File 'lib/codex_sdk/exec.rb', line 13

def pid
  @pid
end

Instance Method Details

#interruptObject

Sends SIGTERM to the subprocess, waits, then SIGKILL if needed.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/codex_sdk/exec.rb', line 84

def interrupt
  @mutex.synchronize do
    return unless @wait_thread&.alive?

    begin
      Process.kill("TERM", @wait_thread.pid)
    rescue Errno::ESRCH
      return
    end

    # Wait for graceful shutdown
    unless wait_for_exit(SHUTDOWN_TIMEOUT)
      begin
        Process.kill("KILL", @wait_thread.pid)
      rescue Errno::ESRCH
        # already gone
      end
    end
  end
end

#run(prompt, resume_thread_id: nil, images: [], output_schema_path: nil, &block) ⇒ Object

Spawns the subprocess, writes the prompt, reads JSONL events. Yields each parsed event hash to the block.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/codex_sdk/exec.rb', line 27

def run(prompt, resume_thread_id: nil, images: [], output_schema_path: nil, &block)
  args = build_args(resume_thread_id: resume_thread_id, images: images, output_schema_path: output_schema_path)
  env = build_env
  sessions_root = codex_sessions_root(env)
  started_at = Time.now
  @context_snapshot = nil

  @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(env, *args)

  # Write prompt and close stdin (one-shot, matching TypeScript SDK)
  @stdin.write(prompt.to_s)
  @stdin.close

  # Read stderr in background thread
  stderr_reader = ::Thread.new do
    @stderr.read
  rescue StandardError
    ""
  end

  # Read JSONL from stdout line by line
  @stdout.each_line do |line|
    line = line.strip
    next if line.empty?

    begin
      data = JSON.parse(line)
    rescue JSON::ParserError => e
      raise ParseError.new("Failed to parse event: #{e.message}", line: line)
    end

    event = Events.parse(data)
    block.call(event)
  end

  stderr_buf = stderr_reader.value.to_s
  status = @wait_thread.value

  unless status.success?
    code = status.exitstatus || status.termsig
    truncated = stderr_buf.length > 500 ? "#{stderr_buf[0, 497]}..." : stderr_buf
    raise ExecError.new(
      "Codex exited with code #{code}: #{truncated}",
      exit_code: code,
      stderr: stderr_buf
    )
  end

  @context_snapshot = read_context_snapshot(
    sessions_root: sessions_root,
    started_at: started_at
  )
ensure
  cleanup
end