claude-agent-sdk 1.4.0 → 1.4.1
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 +4 -4
- data/CHANGELOG.md +5 -0
- data/docs/options.md +13 -2
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +59 -10
- data/lib/claude_agent_sdk/types/options.rb +7 -0
- data/lib/claude_agent_sdk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5207d9e2d85cd3a5b1c8a7f7267d74077dd8ed7c856361f7e8689401a5da0b4b
|
|
4
|
+
data.tar.gz: 644c33952692d891e3c46c8ad4d448e52ef06461b79bec66950b3fb24ab6d926
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03ad7fab32ba205b5598c2237dd65a8a1b25786349da7f1c5815eb83ccc516206d89c1bbfaa9e7c2d0ccc2e5cf3256c8fda2cc92a7d3a48eb6a56c24d28678d8
|
|
7
|
+
data.tar.gz: 4b97b7f3cedb844e78a4d04c4d376f776f76c74e2822c6c6253a6aff8bececbfcce04a5c6a54b52d7348e5900987ea9f5179fe773aa763a06dc126a13b2942a6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.1] - 2026-10-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **`process_group: true`: a SIGKILL the SDK sends the CLI also goes to the CLI's process group.** A wedged CLI that the SDK had to kill could not stop its own children, so one still in its group that ignores stdin EOF (a stdio MCP server, for instance) kept running after the session; before `process_group` it would at least have got the terminal's Ctrl-C. The group SIGKILL goes out just before the SIGKILL of the CLI itself, whenever the SDK sends one (a normal teardown after its grace periods and SIGTERM, a cancelled teardown after its short grace, or a CLI that closed stdout and hung), and adds no delay. SIGTERM, including the one sent at interpreter exit, still goes to the CLI process only, so the CLI's own cleanup of its children still runs; a CLI that exits on SIGTERM or on its own gets no group signal, and processes that left its group are not reached. Only sessions with `process_group: true` on Unix are affected. This narrows what 1.4.0 documented ("the SDK still signals the CLI process, not its group"). See [`docs/options.md`](docs/options.md#process_group).
|
|
14
|
+
|
|
10
15
|
## [1.4.0] - 2026-10-25
|
|
11
16
|
|
|
12
17
|
### Added
|
data/docs/options.md
CHANGED
|
@@ -211,10 +211,21 @@ A plain script that does not trap INT is no worse off: Ctrl-C still raises `Inte
|
|
|
211
211
|
When the CLI process stops:
|
|
212
212
|
|
|
213
213
|
- On `Client#disconnect`, or at the end of a `Client.open` block or a `query()` call, the usual teardown runs: the SDK closes the CLI's stdin, gives it a grace period to finish, then sends SIGTERM and finally SIGKILL.
|
|
214
|
-
- When the interpreter exits normally without a disconnect, an `at_exit` handler sends SIGTERM to the CLI process. Best effort: it does not wait for the turn.
|
|
214
|
+
- When the interpreter exits normally without a disconnect, an `at_exit` handler sends SIGTERM to the CLI process only, not its group. Best effort: it does not wait for the turn, and sends no SIGKILL.
|
|
215
215
|
- When your process dies abruptly (`exit!`, SIGKILL), the SDK cleans up nothing. The CLI sees its stdin close, which normally lets it wind down; how soon is up to the CLI and its background work (see [`CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`](#claude_code_print_bg_wait_ceiling_ms)).
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
SIGTERM still goes to the CLI process only, so the CLI can stop the processes it started in its own order (it does so for stdio MCP servers). Whenever the SDK sends the CLI SIGKILL, that SIGKILL also goes to the CLI's process group, so processes still in that group die with the CLI instead of outliving it: a CLI that has to be killed cannot stop them itself, and with the option on they no longer get the terminal's SIGINT either. The group SIGKILL follows the SDK's own SIGKILL decisions and adds no delay of its own. The SDK sends the CLI SIGKILL when it is still running:
|
|
218
|
+
|
|
219
|
+
- on a normal teardown (disconnect, the end of a `Client.open` block or a `query()` call): after stdin EOF, a grace period, SIGTERM and a second grace period;
|
|
220
|
+
- when that teardown is itself cancelled before it got there (`Async::Stop`, a task timeout): SIGTERM at once, then SIGKILL after a short grace, with no grace for stdin EOF first;
|
|
221
|
+
- when the CLI has closed its stdout but kept running: after a grace period of its own, SIGTERM and a second grace period.
|
|
222
|
+
|
|
223
|
+
What it does not do:
|
|
224
|
+
|
|
225
|
+
- When the CLI exits on its own, or on SIGTERM, before a SIGKILL is due, the SDK sends its group nothing. Its children are the CLI's to stop; one that ignores the CLI's request is not chased.
|
|
226
|
+
- A process that moved to another process group is not reached.
|
|
227
|
+
- The SDK does not wait for or reap the processes the group SIGKILL reaches: they are the CLI's children, not the SDK's.
|
|
228
|
+
- On Windows nothing changes: the SDK signals the CLI process only.
|
|
218
229
|
|
|
219
230
|
The CLI's group is a background group of the terminal. A process in it that reads from the controlling terminal (a tool command that prompts on the tty) is stopped with SIGTTIN, and so is the rest of the group, the CLI included when the tool shares its group. Whether writing to the terminal stops it too depends on the terminal's TOSTOP setting. The SDK's own traffic with the CLI goes through pipes and is unaffected.
|
|
220
231
|
|
|
@@ -376,14 +376,21 @@ module ClaudeAgentSDK
|
|
|
376
376
|
# process_group: the terminal's Ctrl-C signals the whole foreground
|
|
377
377
|
# process group, so a CLI left in ours ends the turn on its own
|
|
378
378
|
# before the host can choose to interrupt, ask, or ignore it. Its own
|
|
379
|
-
# group keeps that decision with the host.
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
# Absent unless set, like :uid.
|
|
383
|
-
# bounded, and stays in our group.
|
|
379
|
+
# group keeps that decision with the host. SIGTERM still goes to the
|
|
380
|
+
# pid only; a SIGKILL also goes to the group (#kill_process_group).
|
|
381
|
+
# Windows has no :pgroup; :new_pgroup (CREATE_NEW_PROCESS_GROUP) is
|
|
382
|
+
# its equivalent, and Unix rejects it. Absent unless set, like :uid.
|
|
383
|
+
# The `-v` probe is short-lived and bounded, and stays in our group.
|
|
384
384
|
opts[Gem.win_platform? ? :new_pgroup : :pgroup] = true if @options.process_group?
|
|
385
385
|
|
|
386
386
|
@stdin, @stdout, @stderr, @process = Open3.popen3(process_env, *cmd, opts)
|
|
387
|
+
# Recorded from the spawn options themselves, on every spawn: only a
|
|
388
|
+
# process this line started with pgroup: true leads a group of its
|
|
389
|
+
# own (pgid == its pid). Kept by identity, so a KILL site asks about
|
|
390
|
+
# the waiter it holds, never the options, which can change after
|
|
391
|
+
# spawn. Not cleared by #close: a read loop still waiting on the
|
|
392
|
+
# same waiter must get the same answer.
|
|
393
|
+
@group_leader = opts[:pgroup] ? @process : nil
|
|
387
394
|
# The CLI emits UTF-8 regardless of the parent locale. popen3 pipes
|
|
388
395
|
# default to Encoding.default_external (US-ASCII under LANG=C/LC_ALL=C
|
|
389
396
|
# — minimal Docker images, systemd, CI), which makes String#strip on
|
|
@@ -639,7 +646,14 @@ module ClaudeAgentSDK
|
|
|
639
646
|
rescue Timeout::Error
|
|
640
647
|
# SIGTERM didn't work — force kill
|
|
641
648
|
begin
|
|
642
|
-
|
|
649
|
+
kill_process_group(@process.pid) if group_leader?(@process)
|
|
650
|
+
begin
|
|
651
|
+
Process.kill('KILL', @process.pid)
|
|
652
|
+
rescue Errno::ESRCH
|
|
653
|
+
# Already reaped: the group KILL just above reaches the CLI
|
|
654
|
+
# too, and its waiter can finish before this signal. Still
|
|
655
|
+
# wait for that waiter below, as the other KILL sites do.
|
|
656
|
+
end
|
|
643
657
|
@process.value
|
|
644
658
|
rescue StandardError => e
|
|
645
659
|
raise if e.is_a?(Async::TimeoutError)
|
|
@@ -670,9 +684,12 @@ module ClaudeAgentSDK
|
|
|
670
684
|
# inside an ensure during fiber cancellation: synchronous SIGTERM now,
|
|
671
685
|
# then a plain (non-reactor) thread escalates to SIGKILL after a grace
|
|
672
686
|
# period. Open3's Process::Waiter thread keeps reaping, so no zombie is
|
|
673
|
-
# left either way. The alive? guard
|
|
674
|
-
#
|
|
675
|
-
# the
|
|
687
|
+
# left either way. The alive? guard before the delayed KILL is best
|
|
688
|
+
# effort: it narrows the window in which the pid could have been reaped
|
|
689
|
+
# and recycled, but does not close it — the waiter reports alive for a
|
|
690
|
+
# moment after its OS wait has reaped the child, and the child can be
|
|
691
|
+
# reaped between the check and the signal. The group KILL shares the
|
|
692
|
+
# pid KILL's check and instant, and so its window.
|
|
676
693
|
#
|
|
677
694
|
# @api private
|
|
678
695
|
def force_terminate_in_background(process, grace_seconds: 2)
|
|
@@ -684,6 +701,8 @@ module ClaudeAgentSDK
|
|
|
684
701
|
end
|
|
685
702
|
|
|
686
703
|
pid = process.pid
|
|
704
|
+
# Captured now, on the caller's thread, for the KILL thread below.
|
|
705
|
+
kill_group = group_leader?(process)
|
|
687
706
|
begin
|
|
688
707
|
Process.kill('TERM', pid)
|
|
689
708
|
rescue Errno::ESRCH
|
|
@@ -695,7 +714,10 @@ module ClaudeAgentSDK
|
|
|
695
714
|
Thread.new do
|
|
696
715
|
unless process.join(grace_seconds)
|
|
697
716
|
begin
|
|
698
|
-
|
|
717
|
+
if process.alive?
|
|
718
|
+
kill_process_group(pid) if kill_group
|
|
719
|
+
Process.kill('KILL', pid)
|
|
720
|
+
end
|
|
699
721
|
rescue Errno::ESRCH
|
|
700
722
|
# Still wait for the waiter when exit raced the signal.
|
|
701
723
|
end
|
|
@@ -740,6 +762,31 @@ module ClaudeAgentSDK
|
|
|
740
762
|
false
|
|
741
763
|
end
|
|
742
764
|
|
|
765
|
+
# Whether +process+ is the waiter #connect spawned with pgroup: true,
|
|
766
|
+
# i.e. the leader of a process group of its own, with the pid as pgid.
|
|
767
|
+
#
|
|
768
|
+
# @api private
|
|
769
|
+
def group_leader?(process)
|
|
770
|
+
!process.nil? && @group_leader.equal?(process)
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# SIGKILL to the process group the CLI was spawned to lead, sent just
|
|
774
|
+
# before every SIGKILL of the CLI itself (process_group: true only), so
|
|
775
|
+
# its children still in that group die with it instead of outliving a
|
|
776
|
+
# CLI that never got to stop them. SIGTERM stays on the pid: the CLI
|
|
777
|
+
# stops its children itself on TERM, in its own order. Group first, so
|
|
778
|
+
# the still-running CLI keeps the group id in use at the signal. Best
|
|
779
|
+
# effort, and it proves nothing about the CLI: the group can be empty
|
|
780
|
+
# (ESRCH) or still hold other members after the CLI left it, so the pid
|
|
781
|
+
# KILL that follows, its waits and its error handling stay as they are.
|
|
782
|
+
#
|
|
783
|
+
# @api private
|
|
784
|
+
def kill_process_group(pgid)
|
|
785
|
+
Process.kill('KILL', -pgid)
|
|
786
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
787
|
+
nil
|
|
788
|
+
end
|
|
789
|
+
|
|
743
790
|
def write(data)
|
|
744
791
|
raise CLIConnectionError, 'Cannot write to terminated process' if @process && !@process.alive?
|
|
745
792
|
raise CLIConnectionError, "Cannot write to process that exited with error: #{@exit_error}" if @exit_error
|
|
@@ -894,6 +941,7 @@ module ClaudeAgentSDK
|
|
|
894
941
|
# double-wait). Both are non-fatal — the message loop just exits.
|
|
895
942
|
# Snapshot: a concurrent close() nils @process mid-wait.
|
|
896
943
|
process = @process
|
|
944
|
+
kill_group = group_leader?(process)
|
|
897
945
|
returncode = nil
|
|
898
946
|
termsig = nil
|
|
899
947
|
forced_exit = false
|
|
@@ -910,6 +958,7 @@ module ClaudeAgentSDK
|
|
|
910
958
|
begin
|
|
911
959
|
Process.kill('TERM', process.pid)
|
|
912
960
|
unless process_exited_within?(process, EOF_TERM_GRACE_SECONDS)
|
|
961
|
+
kill_process_group(process.pid) if kill_group
|
|
913
962
|
Process.kill('KILL', process.pid)
|
|
914
963
|
# Even KILL can't reap a child stuck in uninterruptible kernel
|
|
915
964
|
# I/O; bound this last wait too rather than block on #value.
|
|
@@ -260,6 +260,13 @@ module ClaudeAgentSDK
|
|
|
260
260
|
# {Client#interrupt} or {Client#disconnect}, called from ordinary code,
|
|
261
261
|
# never from inside a signal trap.
|
|
262
262
|
#
|
|
263
|
+
# Teardown still sends SIGTERM to the CLI process only, so the CLI can
|
|
264
|
+
# stop its own children in order. Whenever the SDK has to SIGKILL the
|
|
265
|
+
# CLI (it is still running after the teardown's graces and SIGTERM, or a
|
|
266
|
+
# cancelled teardown's short grace), that SIGKILL also goes to the CLI's
|
|
267
|
+
# process group, so processes still in it die with the CLI instead of
|
|
268
|
+
# outliving it. Unix only; see docs/options.md for the limits.
|
|
269
|
+
#
|
|
263
270
|
# Applies to the CLI process the default transport starts for the
|
|
264
271
|
# session (`pgroup: true` on Unix, `new_pgroup: true` on Windows), not to
|
|
265
272
|
# the short `claude -v` version probe. SDK only: nothing is sent to the
|