ace-llm-providers-cli 0.32.3 → 0.33.0

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: f6963c3de4deb7472734fbb346f1daa390982fb8442af80cae5f337dc093deeb
4
- data.tar.gz: 3b09154bde9e388cf7ef4f52e2ef17c62f93fee36fa5f1fcea7f9a1533cf0521
3
+ metadata.gz: 7410921d087bbcfa634cec60984b2d578de92d92d84fc0730aa4cae054848b4b
4
+ data.tar.gz: '08b71084f7f13ac67941da17c20e84b34c1dd52673f037a929ac7e896e0c2310'
5
5
  SHA512:
6
- metadata.gz: c8bd1e4a71471c32f71afc4d0912f446eeafc3eb1bfa73cb9db0f2db87c9fe978326d677c285a4b84aba75019b2655e663ee2de836fbc11c942ba9a3abcb6150
7
- data.tar.gz: 98a449b5343719f31c42575f2db82881fd7b9efa6c4c7e0bbd3d4e5b8c4ffefe9739f5fcba09b8e79d8d8d417196c925cdc4b4e12c140a69e08547f71cb2f4ca
6
+ metadata.gz: 92613d00db61f1ad8e8a1e99285c478526b38dd998bd26355a543939ba2453da13f23df97479ae9a4186653fce4b31bd4383dc8fa23ff69c9a887ede90d8c339
7
+ data.tar.gz: 50d6485baf304a089b0311a3f0fe096fc90bb697a1b72b4c43f27d295a21631753e1e1d8be718c89722a71e51d3cbfa296926d49fe46875844d4dd53b4969e0a
@@ -3,11 +3,9 @@ class: Ace::LLM::Providers::CLI::PiClient
3
3
  gem: ace-llm-providers-cli
4
4
  models:
5
5
  # ZAI (default provider)
6
- - zai/glm-4.7
7
- - zai/glm-4.6
8
- - zai/glm-4.5
9
- - zai/glm-5.1
10
- - zai/glm-5-turbo
6
+ - zai/glm-5.3
7
+ - zai/glm-5.3-flash
8
+ - zai/glm-5.3-highspeed
11
9
  # Anthropic
12
10
  - anthropic/claude-opus-4-6
13
11
  - anthropic/claude-opus-4-5
@@ -26,10 +24,11 @@ models:
26
24
  - openai-codex/gpt-5.1
27
25
  aliases:
28
26
  global:
29
- pi: pi:zai/glm-5.1
27
+ pi: pi:zai/glm-5.3-flash
30
28
  model:
31
- glm: zai/glm-5.1
32
- glmturbo: zai/glm-5-turbo
29
+ glm: zai/glm-5.3
30
+ glmflash: zai/glm-5.3-flash
31
+ glmhighspeed: zai/glm-5.3-highspeed
33
32
  gpt: openai-codex/gpt-5.4
34
33
  capabilities:
35
34
  - text_generation
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+ - Hardened `SafeCapture` process group termination for container PID namespaces where background processes reparent to PID 1, ensuring reliable cleanup of descendant processes without ESRCH failures.
12
+
13
+ ## [0.33.0] - 2026-09-04
14
+
15
+ ### Changed
16
+ - Registered the live Pi/Z.AI GLM 5.3 family (`zai/glm-5.3`, `zai/glm-5.3-flash`, `zai/glm-5.3-highspeed`) in the shipped provider registry and removed the obsolete GLM 4.x/5.1/5-turbo entries and `glmturbo` alias.
17
+ - Made `zai/glm-5.3-flash` the routine Pi default: the global `pi` alias and `PiClient` default model now target the flash model, with `glm`, `glmflash`, and `glmhighspeed` model aliases for the three live targets.
18
+
19
+ ### Technical
20
+ - Added focused regression coverage for registry parity between project and shipped defaults, alias and full-slash-selector resolution without truncation, unknown-model fail-closed behavior, and `--no-fallback` preserving exactly one provider/model target.
21
+
22
+ ## [0.32.4] - 2026-09-02
23
+
24
+ ### Technical
25
+ - Included in the coordinated all-package patch release preparation for ACE monorepo review.
10
26
  ## [0.32.3] - 2026-08-29
11
27
 
12
28
  ### Fixed
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Linux normally reparents orphaned grandchildren to PID 1. Container
4
+ # entrypoints do not always reap them, leaving killed descendants as observable
5
+ # zombies. This supervisor becomes a child subreaper before spawning the provider
6
+ # command, so it owns and reaps the complete isolated command group before exit.
7
+ module Ace
8
+ module LLM
9
+ module Providers
10
+ module CLI
11
+ module Molecules
12
+ module ProcessSupervisor
13
+ PR_SET_CHILD_SUBREAPER = 36
14
+
15
+ module_function
16
+
17
+ def run(command)
18
+ enable_child_subreaper
19
+ ready_fd = Integer(ENV.delete("ACE_SAFE_CAPTURE_READY_FD"))
20
+ command_pid = Process.spawn(*command, pgroup: true, ready_fd => :close)
21
+ command_pgid = command_pid
22
+ IO.for_fd(ready_fd).tap { |io| io.write("1"); io.close }
23
+
24
+ Signal.trap("TERM") { terminate_group(command_pgid) }
25
+
26
+ _pid, status = Process.wait2(command_pid)
27
+ terminate_group(command_pgid)
28
+ reap_group(command_pgid)
29
+
30
+ exit(status.exitstatus || 128 + status.termsig)
31
+ end
32
+
33
+ def enable_child_subreaper
34
+ previous_deprecated = Warning[:deprecated]
35
+ Warning[:deprecated] = false
36
+ require "fiddle"
37
+ Warning[:deprecated] = previous_deprecated
38
+
39
+ prctl = Fiddle::Function.new(
40
+ Fiddle::Handle::DEFAULT["prctl"],
41
+ [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG],
42
+ Fiddle::TYPE_INT
43
+ )
44
+ raise "unable to become a child subreaper" unless prctl.call(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0).zero?
45
+ ensure
46
+ Warning[:deprecated] = previous_deprecated unless previous_deprecated.nil?
47
+ end
48
+
49
+ def terminate_group(pgid)
50
+ Process.kill("TERM", -pgid)
51
+ Process.kill("KILL", -pgid)
52
+ rescue Errno::ESRCH, Errno::EPERM
53
+ nil
54
+ end
55
+
56
+ def reap_group(pgid)
57
+ loop { Process.waitpid(-pgid) }
58
+ rescue Errno::ECHILD
59
+ nil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ Ace::LLM::Providers::CLI::Molecules::ProcessSupervisor.run(ARGV) if $PROGRAM_NAME == __FILE__
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "rbconfig"
4
5
 
5
6
  module Ace
6
7
  module LLM
@@ -13,8 +14,9 @@ module Ace
13
14
  # which causes "stream closed in another thread (IOError)" when the
14
15
  # timeout fires while Open3's internal reader threads hold pipe handles.
15
16
  #
16
- # Uses Open3.popen3 + Process.kill so the child process is terminated
17
- # directly — no thread interruption, no IOError.
17
+ # Uses Open3.popen3 and, on Linux, a dedicated child-subreaper supervisor
18
+ # so the complete command tree is terminated and reaped without thread
19
+ # interruption or closed-stream races.
18
20
  class SafeCapture
19
21
  # @param cmd [Array<String>] Command arguments
20
22
  # @param timeout [Integer] Timeout in seconds
@@ -23,7 +25,7 @@ module Ace
23
25
  # @param env [Hash, nil] Environment variables (merged with current env)
24
26
  # @param provider_name [String] Provider name for error messages
25
27
  # @param isolate_process_group [Boolean] Spawn subprocess in isolated process group
26
- # @param cleanup_group_on_exit [Boolean] Best-effort cleanup of descendants on success
28
+ # @param cleanup_group_on_exit [Boolean] Clean up descendants on success
27
29
  # @return [Array(String, String, Process::Status)] [stdout, stderr, status]
28
30
  # @raise [Ace::LLM::ProviderError] on timeout
29
31
  def self.call(cmd, timeout:, stdin_data: nil, chdir: nil, env: nil, provider_name: "CLI",
@@ -35,9 +37,19 @@ module Ace
35
37
  opts[:pgroup] = true if isolate_process_group
36
38
 
37
39
  full_cmd = Array(command_prefix) + cmd
38
- args = env ? [env, *full_cmd] : full_cmd
40
+ supervised = isolate_process_group && cleanup_group_on_exit && RUBY_PLATFORM.include?("linux")
41
+ full_cmd = supervisor_command(full_cmd) if supervised
42
+ ready_reader, ready_writer = IO.pipe if supervised
43
+ opts[ready_writer.fileno] = ready_writer if supervised
44
+ spawn_env = env&.dup || {}
45
+ spawn_env["ACE_SAFE_CAPTURE_READY_FD"] = ready_writer.fileno.to_s if supervised
46
+ args = spawn_env.empty? ? full_cmd : [spawn_env, *full_cmd]
39
47
 
40
48
  Open3.popen3(*args, **opts) do |stdin, stdout, stderr, wait_thr|
49
+ ready_writer&.close
50
+ ready_reader&.read(1)
51
+ ready_reader&.close
52
+
41
53
  pid = wait_thr.pid
42
54
  pgid = safe_getpgid(pid)
43
55
  debug_log(provider_name, "spawn pid=#{pid} pgid=#{pgid || "n/a"}")
@@ -56,8 +68,13 @@ module Ace
56
68
 
57
69
  unless wait_thr.join(normalized_timeout)
58
70
  # Timeout: kill subprocess group (and descendants), then clean up
59
- terminate_subprocess_tree(pid: pid, pgid: pgid, provider_name: provider_name)
60
- wait_thr.join(5)
71
+ terminate_subprocess_tree(
72
+ pid: pid, pgid: pgid, provider_name: provider_name, supervised: supervised
73
+ )
74
+ unless wait_thr.join(5)
75
+ terminate_group_or_pid("KILL", pid, pgid)
76
+ wait_thr.join(5)
77
+ end
61
78
 
62
79
  stdout.close unless stdout.closed?
63
80
  stderr.close unless stderr.closed?
@@ -70,12 +87,15 @@ module Ace
70
87
  end
71
88
 
72
89
  status = wait_thr.value
73
- if isolate_process_group && cleanup_group_on_exit
90
+ if isolate_process_group && cleanup_group_on_exit && !supervised
74
91
  terminate_descendants_after_success(pid: pid, pgid: pgid, provider_name: provider_name)
75
92
  end
76
93
 
77
94
  [out_reader.value, err_reader.value, status]
78
95
  end
96
+ ensure
97
+ ready_reader&.close unless ready_reader&.closed?
98
+ ready_writer&.close unless ready_writer&.closed?
79
99
  end
80
100
 
81
101
  class << self
@@ -99,21 +119,24 @@ module Ace
99
119
  raise ArgumentError, "timeout must be a positive numeric value, got #{value.inspect}"
100
120
  end
101
121
 
102
- def terminate_subprocess_tree(pid:, pgid:, provider_name:)
122
+ def supervisor_command(command)
123
+ supervisor = File.expand_path("process_supervisor.rb", __dir__)
124
+ [RbConfig.ruby, "-W0", supervisor, *command]
125
+ end
126
+
127
+ def terminate_subprocess_tree(pid:, pgid:, provider_name:, supervised: false)
103
128
  debug_log(provider_name, "timeout cleanup pid=#{pid} pgid=#{pgid || "n/a"}")
104
129
  terminate_group_or_pid("TERM", pid, pgid)
105
- sleep(0.1)
106
- terminate_group_or_pid("KILL", pid, pgid)
130
+ terminate_group_or_pid("KILL", pid, pgid) unless supervised
107
131
  end
108
132
 
109
133
  def terminate_descendants_after_success(pid:, pgid:, provider_name:)
110
134
  return unless pgid
111
- return unless group_alive?(pgid)
112
135
 
113
136
  debug_log(provider_name, "post-exit cleanup pgid=#{pgid}")
114
- terminate_group_or_pid("TERM", pid, pgid)
115
- sleep(0.05)
116
- terminate_group_or_pid("KILL", pid, pgid) if group_alive?(pgid)
137
+ safe_kill_group(pgid, "TERM")
138
+ safe_kill_group(pgid, "KILL")
139
+ reap_terminated_group(pgid)
117
140
  end
118
141
 
119
142
  def terminate_group_or_pid(signal, pid, pgid)
@@ -122,6 +145,13 @@ module Ace
122
145
  else
123
146
  Process.kill(signal, pid)
124
147
  end
148
+ true
149
+ rescue Errno::ESRCH, Errno::EPERM
150
+ nil
151
+ end
152
+
153
+ def safe_kill_group(pgid, signal)
154
+ Process.kill(signal, -pgid)
125
155
  rescue Errno::ESRCH, Errno::EPERM
126
156
  nil
127
157
  end
@@ -132,13 +162,10 @@ module Ace
132
162
  nil
133
163
  end
134
164
 
135
- def group_alive?(pgid)
136
- Process.kill(0, -pgid)
137
- true
138
- rescue Errno::ESRCH
139
- false
140
- rescue Errno::EPERM
141
- true
165
+ def reap_terminated_group(pgid)
166
+ loop { Process.waitpid(-pgid) }
167
+ rescue Errno::ECHILD
168
+ nil
142
169
  end
143
170
 
144
171
  def debug_log(provider_name, message)
@@ -27,7 +27,7 @@ module Ace
27
27
  "pi"
28
28
  end
29
29
 
30
- DEFAULT_MODEL = "zai/glm-4.7"
30
+ DEFAULT_MODEL = "zai/glm-5.3-flash"
31
31
 
32
32
  def initialize(model: nil, **options)
33
33
  @model = model || DEFAULT_MODEL
@@ -67,7 +67,9 @@ module Ace
67
67
  # List available Pi models
68
68
  def list_models
69
69
  [
70
- {id: "zai/glm-4.7", name: "GLM 4.7", description: "ZAI default model", context_size: 128_000},
70
+ {id: "zai/glm-5.3", name: "GLM 5.3", description: "ZAI flagship model", context_size: 1_000_000},
71
+ {id: "zai/glm-5.3-flash", name: "GLM 5.3 Flash", description: "ZAI routine default", context_size: 1_000_000},
72
+ {id: "zai/glm-5.3-highspeed", name: "GLM 5.3 Highspeed", description: "ZAI low-latency model", context_size: 1_000_000},
71
73
  {id: "anthropic/claude-opus-4-6", name: "Claude Opus 4.6", description: "Anthropic flagship", context_size: 200_000},
72
74
  {id: "anthropic/claude-sonnet-4-5", name: "Claude Sonnet 4.5", description: "Anthropic balanced", context_size: 200_000},
73
75
  {id: "anthropic/claude-haiku-4-5", name: "Claude Haiku 4.5", description: "Anthropic fast", context_size: 200_000},
@@ -4,7 +4,7 @@ module Ace
4
4
  module LLM
5
5
  module Providers
6
6
  module CLI
7
- VERSION = "0.32.3"
7
+ VERSION = "0.33.0"
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-llm-providers-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.3
4
+ version: 0.33.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-30 00:00:00.000000000 Z
10
+ date: 2026-09-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-llm
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.38'
18
+ version: '0.39'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.38'
25
+ version: '0.39'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -111,6 +111,7 @@ files:
111
111
  - lib/ace/llm/providers/cli/codex_oai_client.rb
112
112
  - lib/ace/llm/providers/cli/gemini_client.rb
113
113
  - lib/ace/llm/providers/cli/molecules/health_checker.rb
114
+ - lib/ace/llm/providers/cli/molecules/process_supervisor.rb
114
115
  - lib/ace/llm/providers/cli/molecules/safe_capture.rb
115
116
  - lib/ace/llm/providers/cli/molecules/session_finder.rb
116
117
  - lib/ace/llm/providers/cli/molecules/skill_name_reader.rb