Skip to content

Commit 4a234c9

Browse files
IgorTavcarclaude
andcommitted
feat: apply 10 community PRs as batch
- anthropics#806: setting_sources=[] truthiness fix - anthropics#803: betas=[]/plugins=[] truthiness fix - anthropics#786: ThinkingBlock missing signature crash fix - anthropics#790: suppress ProcessError when result already received - anthropics#658: capture real stderr in ProcessError - anthropics#791: suppress stale task notifications between turns - anthropics#763: guard malformed CLAUDE_CODE_STREAM_CLOSE_TIMEOUT env var - anthropics#805: delete_session() cascades subagent transcript dir - anthropics#804: top-level skills option on ClaudeAgentOptions - anthropics#691: PostCompact hook event type support 479 tests passing, mypy clean, ruff clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 64e8dee commit 4a234c9

15 files changed

Lines changed: 966 additions & 30 deletions

COMMUNITY_BATCH_2026-04-10.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# Community Batch — 2026-04-10
2+
3+
Upstream state at time of application: `64e8dee` (SDK v0.1.58, CLI 2.1.100).
4+
Branch: `community-fixes` (reset to upstream/main, then patched).
5+
6+
This document describes every change cherry-picked from open community PRs
7+
on `anthropics/claude-agent-sdk-python` and applied as a single batch.
8+
9+
---
10+
11+
## 1. PR #806`setting_sources=[]` truthiness fix
12+
13+
**Issue:** #794
14+
**File:** `src/claude_agent_sdk/_internal/transport/subprocess_cli.py`
15+
16+
### Problem
17+
18+
`_build_command()` used `if self._options.setting_sources:` — a truthiness
19+
check. In Python, `[]` is falsy, so passing `setting_sources=[]` (meaning
20+
"load no setting sources") silently did nothing. The CLI fell back to loading
21+
all sources (user, project, local).
22+
23+
### Fix
24+
25+
Changed to `if self._options.setting_sources is not None:`. When the list is
26+
empty, the SDK now sends `--setting-sources ""` to the CLI, which correctly
27+
disables all setting sources.
28+
29+
### Test changes
30+
31+
- `test_build_command_setting_sources_omitted_when_empty` renamed to
32+
`test_build_command_setting_sources_empty_list_passes_empty_string` and
33+
updated to assert that `--setting-sources` IS present with value `""`.
34+
35+
---
36+
37+
## 2. PR #803`betas=[]` and `plugins=[]` truthiness fix
38+
39+
**File:** `src/claude_agent_sdk/_internal/transport/subprocess_cli.py`
40+
41+
### Problem
42+
43+
Same class of bug as #806. `betas` and `plugins` used truthiness checks,
44+
making empty lists indistinguishable from `None` (unset).
45+
46+
### Fix
47+
48+
- `betas`: changed to `if self._options.betas is not None:`. Empty list sends
49+
`--betas ""`.
50+
- `plugins`: changed to `if self._options.plugins is not None:`. Empty list
51+
simply iterates zero times (no behavioral change, but semantically correct).
52+
53+
---
54+
55+
## 3. PR #786 — ThinkingBlock missing `signature` field
56+
57+
**File:** `src/claude_agent_sdk/_internal/message_parser.py`
58+
59+
### Problem
60+
61+
`message_parser.py:110` used `block["signature"]` — a direct dict lookup.
62+
The Anthropic API can return thinking blocks without a `signature` in certain
63+
conditions (redacted thinking blocks, streaming edge cases, older cached
64+
responses), causing a `KeyError` crash.
65+
66+
### Fix
67+
68+
One-line change: `block["signature"]` -> `block.get("signature", "")`.
69+
70+
### Test added
71+
72+
- `test_parse_assistant_message_with_thinking_missing_signature` — verifies
73+
parsing succeeds with an empty string default for the signature field.
74+
75+
---
76+
77+
## 4. PR #790 — Suppress `ProcessError` when result already received
78+
79+
**File:** `src/claude_agent_sdk/_internal/query.py`
80+
81+
### Problem
82+
83+
When using StructuredOutput (or any tool_use stop reason), the CLI exits with
84+
code 1 because it called a tool but received no tool_result response. The
85+
transport raises `ProcessError` after the process exits, even though a valid
86+
`ResultMessage` was already streamed to the consumer.
87+
88+
### Fix
89+
90+
Added a `except ProcessError` handler in `_read_messages()` that checks
91+
`_first_result_event.is_set()`. If a result was already received, the error
92+
is logged as a warning and suppressed. If no result was received, the error
93+
propagates normally.
94+
95+
### Import added
96+
97+
`from .._errors import ProcessError` — needed in `query.py` to catch the
98+
specific exception type.
99+
100+
---
101+
102+
## 5. PR #658 — Capture real stderr in `ProcessError`
103+
104+
**File:** `src/claude_agent_sdk/_internal/transport/subprocess_cli.py`
105+
106+
### Problem
107+
108+
When the CLI subprocess exits non-zero, `ProcessError` was raised with
109+
`stderr="Check stderr output for details"` — a hardcoded string. The actual
110+
stderr was only piped when the user explicitly provided a `stderr` callback
111+
or enabled debug mode. This made debugging `Command failed with exit code 1`
112+
failures impossible without forking the SDK.
113+
114+
### Fix
115+
116+
Three changes:
117+
118+
1. **Always pipe stderr**`stderr_dest` is now unconditionally `PIPE`,
119+
removing the conditional `should_pipe_stderr` check. The stderr stream
120+
setup is also unconditional.
121+
122+
2. **Buffer stderr lines** — Added `self._stderr_buffer: list[str] = []`.
123+
Every line read from stderr is appended to this buffer (in addition to
124+
being forwarded to the user callback if one exists).
125+
126+
3. **Use buffer in ProcessError** — When creating `ProcessError` on non-zero
127+
exit, `stderr` is set to `"\n".join(self._stderr_buffer)` instead of the
128+
hardcoded placeholder.
129+
130+
The buffer is cleared on `close()` to prevent memory leaks across reuse.
131+
132+
---
133+
134+
## 6. PR #791 — Suppress stale task notifications between turns
135+
136+
**File:** `src/claude_agent_sdk/client.py`
137+
138+
### Problem (Issue #788)
139+
140+
When a background task (spawned via `run_in_background=True`) completed
141+
between turns, its `TaskNotificationMessage` sat in the shared message
142+
buffer. The next `receive_response()` call would yield it as the first
143+
message — before any response to the new query, causing the model to respond
144+
to stale task context.
145+
146+
### Fix
147+
148+
Added turn-tracking state to `ClaudeSDKClient`:
149+
- `_current_turn: int` — incremented each time a `ResultMessage` is yielded.
150+
- `_task_turn_map: dict[str, int]` — maps task IDs to the turn they started.
151+
152+
`receive_response()` now defers task-lifecycle events that arrive before the
153+
first non-task message of the current turn. Once a non-task message arrives
154+
(proving the CLI is processing the latest query), deferred events are
155+
re-evaluated:
156+
- Notifications for tasks started in a **previous turn** are **dropped**.
157+
- Notifications for tasks started in the **current turn** (or unknown IDs)
158+
are **yielded**.
159+
160+
### Tests added (4)
161+
162+
- `test_stale_notification_before_turn2_is_suppressed`
163+
- `test_notification_arriving_mid_turn_is_yielded`
164+
- `test_turn_counter_increments_and_cleans_map`
165+
- `test_unknown_task_id_notification_is_yielded`
166+
167+
---
168+
169+
## 7. PR #763 — Guard against malformed `CLAUDE_CODE_STREAM_CLOSE_TIMEOUT`
170+
171+
**File:** `src/claude_agent_sdk/client.py`
172+
173+
### Problem
174+
175+
`ClaudeSDKClient.connect()` used `int(os.environ[...])` to parse the timeout
176+
env var. If the value was malformed (e.g., `"60s"`), connection setup raised
177+
`ValueError` before the SDK could initialize.
178+
179+
### Fix
180+
181+
Added `_parse_timeout_ms_from_env()` helper that:
182+
- Returns the default (60000ms) when the env var is unset.
183+
- Uses `float()` parsing with fallback on `TypeError`/`ValueError`.
184+
- Rejects non-finite values (`inf`, `nan`).
185+
186+
### Test added
187+
188+
- `test_connect_with_invalid_timeout_env_falls_back_to_default`
189+
190+
---
191+
192+
## 8. PR #805`delete_session()` cascades subagent transcript directory
193+
194+
**File:** `src/claude_agent_sdk/_internal/session_mutations.py`
195+
196+
### Problem
197+
198+
`delete_session()` only removed the `{session_id}.jsonl` file. The sibling
199+
`{session_id}/` subdirectory (which holds subagent transcripts) was left
200+
behind, leaking disk space.
201+
202+
### Fix
203+
204+
After removing the `.jsonl` file, `shutil.rmtree(path.parent / session_id,
205+
ignore_errors=True)` cleans up the subagent directory. `ignore_errors=True`
206+
because most sessions never spawn subagents, so the directory usually doesn't
207+
exist.
208+
209+
### Import added
210+
211+
`import shutil`
212+
213+
### Test added
214+
215+
- `test_removes_subagent_transcript_dir` — creates a session with a sibling
216+
subagent dir, deletes the session, asserts both are gone.
217+
218+
---
219+
220+
## 9. PR #804 — Top-level `skills` option on `ClaudeAgentOptions`
221+
222+
**Files:**
223+
- `src/claude_agent_sdk/types.py`
224+
- `src/claude_agent_sdk/_internal/transport/subprocess_cli.py`
225+
- `src/claude_agent_sdk/_internal/query.py`
226+
- `src/claude_agent_sdk/_internal/client.py`
227+
- `src/claude_agent_sdk/client.py`
228+
229+
### Problem
230+
231+
Enabling Skills required two non-obvious steps in unrelated fields:
232+
233+
```python
234+
options = ClaudeAgentOptions(
235+
allowed_tools=["Skill"],
236+
setting_sources=["user", "project"],
237+
)
238+
```
239+
240+
### Fix
241+
242+
Added `skills: list[str] | Literal["all"] | None = None` to
243+
`ClaudeAgentOptions`. The SDK now handles the wiring automatically:
244+
245+
- `skills="all"` -> injects `"Skill"` into `allowed_tools`, defaults
246+
`setting_sources` to `["user", "project"]`.
247+
- `skills=["pdf", "docx"]` -> injects `"Skill(pdf)"`, `"Skill(docx)"` into
248+
`allowed_tools`, defaults `setting_sources`.
249+
- `skills=None` -> no-op (default).
250+
251+
The `_apply_skills_defaults()` method in `SubprocessCLITransport` computes
252+
effective values without mutating the original options object.
253+
254+
The skills list is also sent on the `initialize` control request so
255+
supporting CLIs can filter which skills appear in the system prompt.
256+
257+
### Tests added (10)
258+
259+
- Transport tests for all skills scenarios (none, all, empty, named, merge,
260+
preserve user setting_sources, no mutation, no duplicates).
261+
- Query initialize tests for skills list vs. all/None serialization.
262+
263+
---
264+
265+
## 10. PR #691`PostCompact` hook event support
266+
267+
**Files:**
268+
- `src/claude_agent_sdk/types.py`
269+
- `src/claude_agent_sdk/__init__.py`
270+
271+
### Problem
272+
273+
The CLI (v2.1.76+) fires a `PostCompact` event after context compaction
274+
completes, but the Python SDK had no type for it.
275+
276+
### Fix
277+
278+
Added to `types.py`:
279+
- `PostCompactHookInput` — with `trigger` (`"manual"` | `"auto"`) and
280+
`compact_summary` fields.
281+
- `PostCompactHookSpecificOutput` — with `additionalContext` field.
282+
- Added `"PostCompact"` to the `HookEvent` union.
283+
- Added both types to the `HookInput` and `HookSpecificOutput` unions.
284+
285+
Exported both new types from `__init__.py` and added them to `__all__`.
286+
287+
### Tests added (3)
288+
289+
- `test_post_compact_hook_input` (auto trigger)
290+
- `test_post_compact_hook_input_manual_trigger`
291+
- `test_post_compact_hook_specific_output`
292+
293+
---
294+
295+
## Verification
296+
297+
After applying all changes:
298+
299+
| Check | Result |
300+
|-------|--------|
301+
| `ruff check` | Clean |
302+
| `ruff format` | Clean |
303+
| `mypy src/` | Success: no issues found in 15 source files |
304+
| `pytest tests/` | **479 passed** (20 new tests) |

src/claude_agent_sdk/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
PermissionResultAllow,
7171
PermissionResultDeny,
7272
PermissionUpdate,
73+
PostCompactHookInput,
74+
PostCompactHookSpecificOutput,
7375
PostToolUseFailureHookInput,
7476
PostToolUseFailureHookSpecificOutput,
7577
PostToolUseHookInput,
@@ -554,6 +556,8 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
554556
"StopHookInput",
555557
"SubagentStopHookInput",
556558
"PreCompactHookInput",
559+
"PostCompactHookInput",
560+
"PostCompactHookSpecificOutput",
557561
"NotificationHookInput",
558562
"SubagentStartHookInput",
559563
"PermissionRequestHookInput",

src/claude_agent_sdk/_internal/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ async def process_query(
128128
initialize_timeout=initialize_timeout,
129129
agents=agents_dict,
130130
exclude_dynamic_sections=exclude_dynamic_sections,
131+
skills=configured_options.skills,
131132
)
132133

133134
try:

src/claude_agent_sdk/_internal/message_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def parse_message(data: dict[str, Any]) -> Message | None:
107107
content_blocks.append(
108108
ThinkingBlock(
109109
thinking=block["thinking"],
110-
signature=block["signature"],
110+
signature=block.get("signature", ""),
111111
)
112112
)
113113
case "tool_use":

0 commit comments

Comments
 (0)