Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-langgraph-sdk for 
openSUSE:Factory checked in at 2026-10-17 21:26:59
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langgraph-sdk (Old)
 and      /work/SRC/openSUSE:Factory/.python-langgraph-sdk.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langgraph-sdk"

Sat Oct 17 21:26:59 2026 rev:8 rq:1385355 version:0.4.8

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-langgraph-sdk/python-langgraph-sdk.changes    
    2026-10-15 12:25:38.920898128 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-langgraph-sdk.new.3698736/python-langgraph-sdk.changes
   2026-10-17 21:27:03.053006716 +0200
@@ -1,0 +2,9 @@
+Sat Oct 17 06:32:27 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.4.8:
+  * Thread-stream protocol error frames surface as errors
+    instead of silent closes
+  * Lifecycle watcher starts with run.start and is guarded
+    against races
+
+-------------------------------------------------------------------

Old:
----
  langgraph_sdk-0.4.7.tar.gz

New:
----
  langgraph_sdk-0.4.8.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-langgraph-sdk.spec ++++++
--- /var/tmp/diff_new_pack.Cm6DIA/_old  2026-10-17 21:27:03.773036758 +0200
+++ /var/tmp/diff_new_pack.Cm6DIA/_new  2026-10-17 21:27:03.777036924 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-langgraph-sdk
-Version:        0.4.7
+Version:        0.4.8
 Release:        0
 Summary:        SDK for interacting with the LangGraph API
 License:        MIT

++++++ langgraph_sdk-0.4.7.tar.gz -> langgraph_sdk-0.4.8.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/PKG-INFO 
new/langgraph_sdk-0.4.8/PKG-INFO
--- old/langgraph_sdk-0.4.7/PKG-INFO    2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/PKG-INFO    2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: langgraph-sdk
-Version: 0.4.7
+Version: 0.4.8
 Summary: SDK for interacting with LangGraph API
 Project-URL: Source, 
https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-py
 Project-URL: Twitter, https://x.com/langchain_oss
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/langgraph_sdk/__init__.py 
new/langgraph_sdk-0.4.8/langgraph_sdk/__init__.py
--- old/langgraph_sdk-0.4.7/langgraph_sdk/__init__.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/langgraph_sdk/__init__.py   2020-02-02 
01:00:00.000000000 +0100
@@ -3,7 +3,7 @@
 from langgraph_sdk.encryption import Encryption
 from langgraph_sdk.encryption.types import DecryptResult, EncryptionContext
 
-__version__ = "0.4.7"
+__version__ = "0.4.8"
 
 __all__ = [
     "Auth",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/langgraph_sdk/_async/stream.py 
new/langgraph_sdk-0.4.8/langgraph_sdk/_async/stream.py
--- old/langgraph_sdk-0.4.7/langgraph_sdk/_async/stream.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/langgraph_sdk/_async/stream.py      2020-02-02 
01:00:00.000000000 +0100
@@ -15,6 +15,7 @@
 
 import asyncio
 import contextlib
+import logging
 import random
 from collections.abc import AsyncGenerator, AsyncIterator, Generator, Mapping
 from dataclasses import dataclass, field
@@ -36,6 +37,11 @@
     ToolCallsDecoder,
     validate_interleave_channels,
 )
+from langgraph_sdk.stream.errors import (
+    is_non_retryable_stream_error,
+    protocol_error,
+    stream_error_frame,
+)
 from langgraph_sdk.stream.subscription import compute_union_filter, 
infer_channel
 from langgraph_sdk.stream.transport import (
     AsyncProtocolTransport,
@@ -44,6 +50,8 @@
     ProtocolWebSocketTransport,
 )
 
+logger = logging.getLogger(__name__)
+
 
 class InterruptPayload(TypedDict):
     """Payload surfaced when the server requests human input for a thread."""
@@ -193,6 +201,7 @@
             if not gate.done():
                 gate.set_result(None)
             self._owner._run_seen = True
+            self._owner._ensure_lifecycle_watcher_running()
             return result
         except BaseException as err:
             # Why: gate MUST reject on any exit type, including CancelledError,
@@ -1203,6 +1212,7 @@
         # observes a `completed`/`errored` event.
         self._interrupts_lock = asyncio.Lock()
         self._lifecycle_watcher_task: asyncio.Task[None] | None = None
+        self._lifecycle_watcher_rejected = False
         self._lifecycle_watcher_handle: EventStreamHandle | None = None
         self._lifecycle_cursor: int | None = None
         self._lifecycle_max_reconnect_attempts = 5
@@ -1261,8 +1271,11 @@
         # Create the run-done future here (async context guarantees a running 
loop).
         self._run_done = asyncio.get_running_loop().create_future()
         # Start the lifecycle watcher immediately so reattach and thread.output
-        # work without a preceding run.start call.
-        self._ensure_lifecycle_watcher_running()
+        # work without a preceding run.start call. A minted thread_id names no
+        # thread until run.start creates it, and with custom auth a stream for 
a
+        # thread that doesn't exist yet fails, so run.start starts the watcher.
+        if self._explicit_thread_id:
+            self._ensure_lifecycle_watcher_running()
         return self
 
     async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
@@ -1630,6 +1643,13 @@
                 async for event in self._dedup_iter(shared.events):
                     if self._closed:
                         break
+                    if (frame_error := stream_error_frame(event)) is not None:
+                        logger.warning(
+                            "Thread %s event stream failed: %s",
+                            self.thread_id,
+                            frame_error,
+                        )
+                        continue
                     self._observe_event(event)
                     for sub in list(self._subscriptions.values()):
                         if matches_subscription(event, sub.params):
@@ -1655,6 +1675,7 @@
                 if (
                     err is not None
                     and not isinstance(err, asyncio.CancelledError)
+                    and not is_non_retryable_stream_error(err)
                     and not self._closed
                 ):
                     with contextlib.suppress(Exception):
@@ -1681,7 +1702,8 @@
 
         Returns:
             `True` if a new stream was opened (caller should resume fanout),
-            `False` if all reconnect attempts were exhausted or the controller
+            `False` if all reconnect attempts were exhausted, the server
+            rejected the stream with a non-retryable 4xx, or the controller
             was closed in the meantime.
         """
         if self._transport is None:
@@ -1702,7 +1724,9 @@
                 await new_stream.ready
             except asyncio.CancelledError:
                 raise
-            except Exception:
+            except Exception as err:
+                if is_non_retryable_stream_error(err):
+                    return False
                 await self._reconnect_sleep(attempt)
                 continue
             self._shared_stream = new_stream
@@ -1800,9 +1824,7 @@
             # 202/204 — no body. Caller gets an empty result.
             return {}
         if response.get("type") == "error":
-            code = response.get("error", "unknown")
-            message = response.get("message", "")
-            raise RuntimeError(f"Protocol error [{code}]: {message}")
+            raise protocol_error(response)
         meta = response.get("meta")
         if isinstance(meta, dict):
             applied_through_seq = meta.get("applied_through_seq")
@@ -1826,8 +1848,15 @@
             await asyncio.wait_for(asyncio.shield(gate), timeout=timeout)
 
     def _ensure_lifecycle_watcher_running(self) -> None:
-        if self._lifecycle_watcher_task is not None:
+        task = self._lifecycle_watcher_task
+        if task is not None and not self._lifecycle_watcher_rejected:
             return
+        if task is not None:
+            # A 4xx stopped the watcher before this run started, typically the
+            # 404 for a thread this run.start just created, so the result it
+            # left isn't this run's.
+            self._run_done = asyncio.get_running_loop().create_future()
+            self._lifecycle_watcher_rejected = False
         self._lifecycle_watcher_task = asyncio.create_task(
             self._run_lifecycle_watcher()
         )
@@ -1850,7 +1879,11 @@
         if self._transport is None:
             return
         reconnect_attempts = 0
+        frame_error: RuntimeError | None = None
         while not self._closed:
+            # A stream opened before run.start can 404 for the thread that
+            # run.start creates, so it gets one more try once a run has 
started.
+            opened_before_run = not self._run_seen
             try:
                 handle = self._transport.open_event_stream(
                     self._lifecycle_stream_params()
@@ -1860,6 +1893,7 @@
                 async for event in handle.events:
                     if self._closed:
                         return
+                    frame_error = stream_error_frame(event) or frame_error
                     self._observe_lifecycle_event(event)
                     await self._apply_lifecycle_event(event)
                 err = await handle.done
@@ -1868,38 +1902,43 @@
                     # event. Resolve `_run_done` as errored so awaiters of
                     # `thread.output` don't hang.
                     if err is None:
-                        run_done = self._run_done
-                        if run_done is not None and not run_done.done():
-                            run_done.set_result(
-                                _RunTerminal(
-                                    status="errored",
-                                    error=RuntimeError(
-                                        "lifecycle stream ended before 
terminal event"
-                                    ),
-                                )
+                        self._fail_run_done(
+                            frame_error
+                            or RuntimeError(
+                                "lifecycle stream ended before terminal event"
                             )
+                        )
                     return
                 reconnect_attempts += 1
-                if reconnect_attempts > self._lifecycle_max_reconnect_attempts:
+                if (
+                    is_non_retryable_stream_error(err)
+                    or reconnect_attempts > 
self._lifecycle_max_reconnect_attempts
+                ):
                     raise err
                 await asyncio.sleep(0.05)
             except asyncio.CancelledError:
                 raise
             except Exception as exc:
+                if is_non_retryable_stream_error(exc):
+                    if opened_before_run and self._run_seen:
+                        continue
+                    self._lifecycle_watcher_rejected = True
+                    self._fail_run_done(exc)
+                    return
                 reconnect_attempts += 1
                 if reconnect_attempts <= 
self._lifecycle_max_reconnect_attempts:
                     await asyncio.sleep(0.05)
                     continue
-                run_done = self._run_done
-                if run_done is not None and not run_done.done():
-                    run_done.set_result(
-                        _RunTerminal(
-                            status="errored",
-                            error=RuntimeError(f"Lifecycle transport failed: 
{exc}"),
-                        )
-                    )
+                self._fail_run_done(
+                    frame_error or RuntimeError(f"Lifecycle transport failed: 
{exc}")
+                )
                 return
 
+    def _fail_run_done(self, error: BaseException) -> None:
+        run_done = self._run_done
+        if run_done is not None and not run_done.done():
+            run_done.set_result(_RunTerminal(status="errored", error=error))
+
     async def _fetch_state(self) -> dict[str, Any]:
         """Fetch the current thread state from the REST endpoint."""
         return await self._http.get(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/langgraph_sdk/_sync/stream.py 
new/langgraph_sdk-0.4.8/langgraph_sdk/_sync/stream.py
--- old/langgraph_sdk-0.4.7/langgraph_sdk/_sync/stream.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/langgraph_sdk/_sync/stream.py       2020-02-02 
01:00:00.000000000 +0100
@@ -34,6 +34,11 @@
     ToolCallsDecoder,
     validate_interleave_channels,
 )
+from langgraph_sdk.stream.errors import (
+    is_non_retryable_stream_error,
+    protocol_error,
+    stream_error_frame,
+)
 from langgraph_sdk.stream.subscription import compute_union_filter, 
infer_channel
 from langgraph_sdk.stream.sync_controller import (
     SyncStreamController,
@@ -237,6 +242,7 @@
         controller = self._owner._controller
         if controller is not None and controller._run_start_gate is not None:
             controller._run_start_gate.set()
+        self._owner._ensure_lifecycle_watcher_running()
         return result
 
     def respond(
@@ -1111,6 +1117,7 @@
         self.interrupted: bool = False
         self.interrupts: list[InterruptPayload] = []
         self._lifecycle_watcher_thread: threading.Thread | None = None
+        self._lifecycle_watcher_lock = threading.Lock()
         self._lifecycle_watcher_handle: SyncEventStreamHandle | None = None
         self._lifecycle_cursor: int | None = None
         self._lifecycle_max_reconnect_attempts = 5
@@ -1149,7 +1156,11 @@
             self._transport, run_start_gate=run_start_gate
         )
         self._run_done = _BlockingResult()
-        self._ensure_lifecycle_watcher_running()
+        # A minted thread_id names no thread until run.start creates it, and
+        # with custom auth a stream for a thread that doesn't exist yet fails,
+        # so run.start starts the watcher instead.
+        if self._explicit_thread_id:
+            self._ensure_lifecycle_watcher_running()
         return self
 
     def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
@@ -1485,9 +1496,7 @@
         if response is None:
             return {}
         if response.get("type") == "error":
-            code = response.get("error", "unknown")
-            message = response.get("message", "")
-            raise RuntimeError(f"Protocol error [{code}]: {message}")
+            raise protocol_error(response)
         meta = response.get("meta")
         if isinstance(meta, dict):
             applied_through_seq = meta.get("applied_through_seq")
@@ -1496,14 +1505,20 @@
         return response.get("result", {})
 
     def _ensure_lifecycle_watcher_running(self) -> None:
-        if self._lifecycle_watcher_thread is not None:
-            return
-        self._lifecycle_watcher_thread = threading.Thread(
-            target=self._run_lifecycle_watcher,
-            name="langgraph-sdk-sync-lifecycle",
-            daemon=True,
-        )
-        self._lifecycle_watcher_thread.start()
+        with self._lifecycle_watcher_lock:
+            if self._lifecycle_watcher_thread is not None:
+                return
+            if self._run_done is not None and self._run_done.done():
+                # A 4xx stopped the watcher before this run started, typically
+                # the 404 for a thread this run.start just created, so the
+                # result it left isn't this run's.
+                self._run_done = _BlockingResult()
+            self._lifecycle_watcher_thread = threading.Thread(
+                target=self._run_lifecycle_watcher,
+                name="langgraph-sdk-sync-lifecycle",
+                daemon=True,
+            )
+            self._lifecycle_watcher_thread.start()
 
     def _observe_lifecycle_event(self, event: Event) -> None:
         seq = event.get("seq")
@@ -1523,7 +1538,11 @@
         if self._transport is None:
             return
         reconnect_attempts = 0
+        frame_error: RuntimeError | None = None
         while not self._closed:
+            # A stream opened before run.start can 404 for the thread that
+            # run.start creates, so it gets one more try once a run has 
started.
+            opened_before_run = not self._run_seen
             try:
                 handle = self._transport.open_event_stream(
                     self._lifecycle_stream_params()
@@ -1532,28 +1551,64 @@
                 for event in handle.events:
                     if self._closed:
                         return
+                    frame_error = stream_error_frame(event) or frame_error
                     self._observe_lifecycle_event(event)
                     self._apply_lifecycle_event(event)
                 err = handle.error()
                 if err is None:
+                    # Clean EOF: stream ended without a terminal lifecycle
+                    # event. Resolve `_run_done` as errored so `thread.output`
+                    # doesn't block forever.
+                    if not self._closed:
+                        self._fail_run_done(
+                            frame_error
+                            or RuntimeError(
+                                "lifecycle stream ended before terminal event"
+                            )
+                        )
                     return
                 reconnect_attempts += 1
-                if reconnect_attempts > self._lifecycle_max_reconnect_attempts:
+                if (
+                    is_non_retryable_stream_error(err)
+                    or reconnect_attempts > 
self._lifecycle_max_reconnect_attempts
+                ):
                     raise err
             except Exception as exc:
+                if is_non_retryable_stream_error(exc):
+                    if self._stop_rejected_lifecycle_watcher(
+                        exc, unless_run_started=opened_before_run
+                    ):
+                        return
+                    continue
                 reconnect_attempts += 1
                 if reconnect_attempts <= 
self._lifecycle_max_reconnect_attempts:
                     continue
-                run_done = self._run_done
-                if run_done is not None and not run_done.done():
-                    run_done.set_result(
-                        _RunTerminal(
-                            status="errored",
-                            error=RuntimeError(f"Lifecycle transport failed: 
{exc}"),
-                        )
-                    )
+                self._fail_run_done(
+                    frame_error or RuntimeError(f"Lifecycle transport failed: 
{exc}")
+                )
                 return
 
+    def _stop_rejected_lifecycle_watcher(
+        self, error: BaseException, *, unless_run_started: bool = False
+    ) -> bool:
+        """Fail the run with a 4xx `error` and clear the watcher, so run.start 
starts another.
+
+        Returns False, changing nothing, when `unless_run_started` is set and a
+        run has started. Holding the lock keeps a run.start from seeing this
+        watcher as still running once it has decided to stop.
+        """
+        with self._lifecycle_watcher_lock:
+            if unless_run_started and self._run_seen:
+                return False
+            self._fail_run_done(error)
+            self._lifecycle_watcher_thread = None
+            return True
+
+    def _fail_run_done(self, error: BaseException) -> None:
+        run_done = self._run_done
+        if run_done is not None and not run_done.done():
+            run_done.set_result(_RunTerminal(status="errored", error=error))
+
     def _fetch_state(self) -> dict[str, Any]:
         """Fetch the current thread state from the REST endpoint."""
         return self._http.get(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/langgraph_sdk/stream/errors.py 
new/langgraph_sdk-0.4.8/langgraph_sdk/stream/errors.py
--- old/langgraph_sdk-0.4.7/langgraph_sdk/stream/errors.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/langgraph_sdk/stream/errors.py      2020-02-02 
01:00:00.000000000 +0100
@@ -0,0 +1,46 @@
+"""Thread-stream failures, shared by the async and sync clients."""
+
+from __future__ import annotations
+
+from typing import Any
+
+import httpx
+from websockets.exceptions import ConnectionClosed, InvalidStatus
+
+
+def protocol_error(response: dict[str, Any]) -> RuntimeError:
+    """Build the error for a protocol `{"type": "error"}` envelope."""
+    code = response.get("error", "unknown")
+    message = response.get("message", "")
+    return RuntimeError(f"Protocol error [{code}]: {message}")
+
+
+def stream_error_frame(event: Any) -> RuntimeError | None:
+    """Return the error in an unsolicited `{"type": "error", "id": null}` 
frame.
+
+    The server sends one just before it closes a thread stream whose read 
failed.
+    """
+    if (
+        isinstance(event, dict)
+        and event.get("type") == "error"
+        and event.get("id") is None
+    ):
+        return protocol_error(event)
+    return None
+
+
+def is_non_retryable_stream_error(error: BaseException | None) -> bool:
+    """Whether reopening the stream would fail the same way.
+
+    True for a 4xx other than 408 and 429: the status of a refused stream 
request
+    or WebSocket upgrade, or a WebSocket close code of 4000 plus that status. A
+    server that rejects a WebSocket after accepting it closes with such a code,
+    because a browser can't read the status of a refused upgrade.
+    """
+    if isinstance(error, (httpx.HTTPStatusError, InvalidStatus)):
+        status = error.response.status_code
+    elif isinstance(error, ConnectionClosed) and error.rcvd is not None:
+        status = error.rcvd.code - 4000
+    else:
+        return False
+    return 400 <= status < 500 and status not in (408, 429)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langgraph_sdk-0.4.7/langgraph_sdk/stream/sync_controller.py 
new/langgraph_sdk-0.4.8/langgraph_sdk/stream/sync_controller.py
--- old/langgraph_sdk-0.4.7/langgraph_sdk/stream/sync_controller.py     
2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/langgraph_sdk/stream/sync_controller.py     
2020-02-02 01:00:00.000000000 +0100
@@ -13,6 +13,10 @@
 
 from langchain_protocol import Event, SubscribeParams
 
+from langgraph_sdk.stream.errors import (
+    is_non_retryable_stream_error,
+    stream_error_frame,
+)
 from langgraph_sdk.stream.subscription import compute_union_filter, 
filter_covers
 from langgraph_sdk.stream.transport import (
     SyncEventStreamHandle,
@@ -167,6 +171,13 @@
                 return
             try:
                 for event in self._dedup_iter(shared.events):
+                    if (frame_error := stream_error_frame(event)) is not None:
+                        _logger.warning(
+                            "Thread %s event stream failed: %s",
+                            self._transport.thread_id,
+                            frame_error,
+                        )
+                        continue
                     with self._lock:
                         if self._closed:
                             break
@@ -190,7 +201,11 @@
                     continue  # rotation happened; pick up new stream
                 # No rotation — check if this was a transport drop
             err = shared.error()
-            if err is not None and not self._closed:
+            if (
+                err is not None
+                and not is_non_retryable_stream_error(err)
+                and not self._closed
+            ):
                 if self._reconnect_shared_stream():
                     continue
             break
@@ -199,25 +214,6 @@
             for sub in self._subscriptions.values():
                 sub.queue.put(None)
 
-    def _reconnect_shared_stream(self) -> bool:
-        with self._lock:
-            base_filter = self._shared_stream_filter
-        if base_filter is None:
-            return False
-        for _ in range(self._max_reconnect_attempts):
-            with self._lock:
-                if self._closed:
-                    return False
-                params = self._filter_with_since(base_filter)
-            try:
-                new_stream = self._transport.open_event_stream(params)
-            except Exception:
-                continue
-            with self._lock:
-                self._shared_stream = new_stream
-            return True
-        return False
-
     def _compute_current_union(
         self, extra: SubscribeParams | None = None
     ) -> dict[str, Any]:
@@ -296,7 +292,8 @@
         """Attempt to reopen the shared stream after a transport drop.
 
         Returns True if a new stream was successfully opened, False if all
-        reconnect attempts were exhausted or the controller was closed.
+        reconnect attempts were exhausted, the server rejected the stream with 
a
+        non-retryable 4xx, or the controller was closed.
         """
         base_filter = self._shared_stream_filter
         if base_filter is None:
@@ -317,6 +314,8 @@
                         old.close()
                 return True
             except Exception as err:
+                if is_non_retryable_stream_error(err):
+                    return False
                 _logger.debug("sync reconnect attempt %d failed: %r", attempt, 
err)
         return False
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/tests/streaming/_events.py 
new/langgraph_sdk-0.4.8/tests/streaming/_events.py
--- old/langgraph_sdk-0.4.7/tests/streaming/_events.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/_events.py  2020-02-02 
01:00:00.000000000 +0100
@@ -69,6 +69,16 @@
     return _base(seq, "lifecycle", namespace or [], {"event": "failed", 
"error": error})
 
 
+def thread_stream_failed_frame() -> dict[str, Any]:
+    """Unsolicited error frame the server sends before closing a failed 
stream."""
+    return {
+        "type": "error",
+        "id": None,
+        "error": "unknown_error",
+        "message": "Thread event stream failed.",
+    }
+
+
 def values_event(
     seq: int = 0, namespace: list[str] | None = None, **data: Any
 ) -> dict[str, Any]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/tests/streaming/_fake_server.py 
new/langgraph_sdk-0.4.8/tests/streaming/_fake_server.py
--- old/langgraph_sdk-0.4.7/tests/streaming/_fake_server.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/_fake_server.py     2020-02-02 
01:00:00.000000000 +0100
@@ -60,6 +60,7 @@
     events: list[dict[str, Any]]
     delay: float = 0.0
     fail_after: int | None = None
+    status: int = 200
 
 
 class FakeServer:
@@ -263,6 +264,8 @@
                     events=list(self.scripted_events), delay=self._stream_delay
                 )
             )
+            if script.status != 200:
+                return httpx.Response(script.status, json={"detail": 
"scripted"})
             self.open_event_streams += 1
             self._open_event_streams_max = max(
                 self._open_event_streams_max, self.open_event_streams
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langgraph_sdk-0.4.7/tests/streaming/_sync_fake_server.py 
new/langgraph_sdk-0.4.8/tests/streaming/_sync_fake_server.py
--- old/langgraph_sdk-0.4.7/tests/streaming/_sync_fake_server.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/_sync_fake_server.py        
2020-02-02 01:00:00.000000000 +0100
@@ -14,6 +14,7 @@
 class SyncStreamScript:
     events: list[dict[str, Any]]
     fail_after: int | None = None
+    status: int = 200
 
 
 class _SseByteStream(httpx.SyncByteStream):
@@ -113,6 +114,8 @@
                 if self._stream_scripts
                 else SyncStreamScript(events=list(self.scripted_events))
             )
+            if script.status != 200:
+                return httpx.Response(script.status, json={"detail": 
"scripted"})
             return httpx.Response(
                 200,
                 headers={"content-type": "text/event-stream"},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langgraph_sdk-0.4.7/tests/streaming/test_lifecycle_watcher.py 
new/langgraph_sdk-0.4.8/tests/streaming/test_lifecycle_watcher.py
--- old/langgraph_sdk-0.4.7/tests/streaming/test_lifecycle_watcher.py   
2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/test_lifecycle_watcher.py   
2020-02-02 01:00:00.000000000 +0100
@@ -17,6 +17,7 @@
     lifecycle_completed_event,
     lifecycle_errored_event,
     lifecycle_event,
+    thread_stream_failed_frame,
 )
 from streaming._fake_server import FakeServer, _StreamScript
 
@@ -48,7 +49,7 @@
 
 
 async def test_aenter_starts_lifecycle_watcher():
-    """Entering AsyncThreadStream opens lifecycle/input SSE before 
run.start."""
+    """Entering with an explicit thread_id opens the lifecycle/input SSE 
before run.start."""
     fake = FakeServer()
     fake.script([lifecycle_event(seq=0, phase="started")])
     asgi = httpx.ASGITransport(app=fake.app)
@@ -274,3 +275,137 @@
     assert fake.stream_request_bodies[0]["channels"] == ["lifecycle", "input"]
     assert fake.stream_request_bodies[1]["channels"] == ["lifecycle", "input"]
     assert fake.stream_request_bodies[1]["since"] == 1
+
+
+async def test_lifecycle_error_frame_becomes_the_run_error():
+    fake = FakeServer()
+    fake.script([lifecycle_event(seq=0, phase="running"), 
thread_stream_failed_frame()])
+    async with httpx.AsyncClient(
+        transport=fake.transport, base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(thread_id="t-1", assistant_id="agent") as 
thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = await asyncio.wait_for(run_done, timeout=2.0)
+
+    assert terminal.status == "errored"
+    assert (
+        str(terminal.error)
+        == "Protocol error [unknown_error]: Thread event stream failed."
+    )
+
+
+async def test_lifecycle_watcher_stops_on_a_404_without_retrying():
+    fake = FakeServer()
+    fake.script_sequence([_StreamScript(events=[], status=404)])
+    async with httpx.AsyncClient(
+        transport=fake.transport, base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(thread_id="t-1", assistant_id="agent") as 
thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = await asyncio.wait_for(run_done, timeout=2.0)
+
+    assert isinstance(terminal.error, httpx.HTTPStatusError)
+    assert terminal.error.response.status_code == 404
+    assert len(fake.stream_request_bodies) == 1
+
+
+async def test_minted_thread_opens_the_lifecycle_stream_after_run_start():
+    fake = FakeServer()
+    fake.script([lifecycle_completed_event(seq=1)])
+    async with httpx.AsyncClient(
+        transport=fake.transport, base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(assistant_id="agent") as thread:
+            assert thread._lifecycle_watcher_task is None
+            await thread.run.start(input={})
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = await asyncio.wait_for(run_done, timeout=2.0)
+
+    assert terminal.status == "completed"
+
+
+def _new_thread_scripts() -> list[_StreamScript]:
+    return [
+        _StreamScript(events=[], status=404),
+        _StreamScript(events=[lifecycle_completed_event(seq=1)]),
+    ]
+
+
+async def 
test_run_start_restarts_a_watcher_stopped_by_a_404_for_a_new_thread():
+    fake = FakeServer()
+    fake.script_sequence(_new_thread_scripts())
+    fake.set_state({"ok": True})
+    async with httpx.AsyncClient(
+        transport=fake.transport, base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(thread_id="t-new", assistant_id="agent") as 
thread:
+            rejected = thread._run_done
+            assert rejected is not None
+            await asyncio.wait_for(rejected, timeout=2.0)
+            await thread.run.start(input={})
+            output = await thread.output.with_timeout(2.0)
+
+    assert output == {"ok": True}
+    assert len(fake.stream_request_bodies) == 2
+
+
+async def test_lifecycle_watcher_retries_a_404_that_arrives_after_run_start():
+    fake = FakeServer()
+    fake.script_sequence(_new_thread_scripts())
+    fake.set_state({"ok": True})
+    stream_opened = asyncio.Event()
+    run_started = asyncio.Event()
+
+    async def handle(request: httpx.Request) -> httpx.Response:
+        if (
+            request.url.path.endswith("/stream/events")
+            and not fake.stream_request_bodies
+        ):
+            stream_opened.set()
+            await run_started.wait()
+        return await fake._handle_request(request)
+
+    async with httpx.AsyncClient(
+        transport=httpx.MockTransport(handle), base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(thread_id="t-new", assistant_id="agent") as 
thread:
+            await asyncio.wait_for(stream_opened.wait(), timeout=2.0)
+            await thread.run.start(input={})
+            run_started.set()
+            output = await thread.output.with_timeout(2.0)
+
+    assert output == {"ok": True}
+    assert len(fake.stream_request_bodies) == 2
+
+
+async def 
test_lifecycle_watcher_reports_the_error_frame_when_reconnects_run_out():
+    fake = FakeServer()
+    async with httpx.AsyncClient(
+        transport=fake.transport, base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        stream = threads.stream(thread_id="t-1", assistant_id="agent")
+        stream._lifecycle_max_reconnect_attempts = 1
+        fake.script_sequence(
+            [
+                _StreamScript(events=[thread_stream_failed_frame()], 
fail_after=1)
+                for _ in range(stream._lifecycle_max_reconnect_attempts + 1)
+            ]
+        )
+        async with stream as thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = await asyncio.wait_for(run_done, timeout=2.0)
+
+    assert (
+        str(terminal.error)
+        == "Protocol error [unknown_error]: Thread event stream failed."
+    )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langgraph_sdk-0.4.7/tests/streaming/test_shared_stream.py 
new/langgraph_sdk-0.4.8/tests/streaming/test_shared_stream.py
--- old/langgraph_sdk-0.4.7/tests/streaming/test_shared_stream.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/test_shared_stream.py       
2020-02-02 01:00:00.000000000 +0100
@@ -1,11 +1,13 @@
 from __future__ import annotations
 
 import asyncio
-from collections.abc import AsyncGenerator
+from collections.abc import AsyncGenerator, AsyncIterator
 from typing import Any, cast
 from unittest.mock import MagicMock
 
 import httpx
+from websockets.exceptions import ConnectionClosedError
+from websockets.frames import Close
 
 from langgraph_sdk._async.http import HttpClient
 from langgraph_sdk._async.threads import ThreadsClient
@@ -348,3 +350,66 @@
     ]
     assert len(values_requests) == 1
     assert values_requests[0]["since"] == 17
+
+
+async def _drain(events: AsyncIterator[Any]) -> list[Any]:
+    return [event async for event in events]
+
+
+async def test_shared_stream_stops_reconnecting_on_a_404():
+    fake = FakeServer()
+    fake.script_sequence(
+        [
+            _StreamScript(events=[values_event(seq=1)], fail_after=1),
+            _StreamScript(events=[], status=404),
+        ]
+    )
+    async with httpx.AsyncClient(
+        transport=fake.transport, base_url="http://test";
+    ) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(assistant_id="agent") as thread:
+            received = await asyncio.wait_for(
+                _drain(thread.subscribe(["values"])), timeout=5.0
+            )
+
+    assert [event["seq"] for event in received] == [1]
+    assert len(fake.stream_request_bodies) == 2
+
+
+async def test_shared_stream_does_not_reconnect_after_a_4404_close(
+    monkeypatch: Any,
+) -> None:
+    opened: list[dict[str, Any]] = []
+
+    def open_then_close_with_4404(
+        _self: ProtocolSseTransport, params: dict[str, Any]
+    ) -> EventStreamHandle:
+        opened.append(params)
+        loop = asyncio.get_running_loop()
+        ready: asyncio.Future[None] = loop.create_future()
+        ready.set_result(None)
+        done: asyncio.Future[BaseException | None] = loop.create_future()
+        done.set_result(ConnectionClosedError(Close(4404, "not found"), None))
+
+        async def events() -> AsyncGenerator[Any, None]:
+            await asyncio.sleep(0)
+            yield values_event(seq=1)
+
+        async def close() -> None:
+            return None
+
+        return EventStreamHandle(events=events(), ready=ready, done=done, 
close=close)
+
+    monkeypatch.setattr(
+        ProtocolSseTransport, "open_event_stream", open_then_close_with_4404
+    )
+    async with httpx.AsyncClient(base_url="http://test";) as raw:
+        threads = ThreadsClient(HttpClient(raw))
+        async with threads.stream(assistant_id="agent") as thread:
+            received = await asyncio.wait_for(
+                _drain(thread.subscribe(["values"])), timeout=5.0
+            )
+
+    assert [event["seq"] for event in received] == [1]
+    assert len(opened) == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langgraph_sdk-0.4.7/tests/streaming/test_stream_errors.py 
new/langgraph_sdk-0.4.8/tests/streaming/test_stream_errors.py
--- old/langgraph_sdk-0.4.7/tests/streaming/test_stream_errors.py       
1970-01-01 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/test_stream_errors.py       
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import httpx
+import pytest
+from websockets.datastructures import Headers
+from websockets.exceptions import ConnectionClosedError, InvalidStatus
+from websockets.frames import Close
+from websockets.http11 import Response
+
+from langgraph_sdk.stream.errors import (
+    is_non_retryable_stream_error,
+    stream_error_frame,
+)
+
+
+def _http_error(status: int) -> httpx.HTTPStatusError:
+    request = httpx.Request("POST", "http://test/threads/t-1/stream/events";)
+    response = httpx.Response(status, request=request)
+    return httpx.HTTPStatusError("scripted", request=request, 
response=response)
+
+
+def _ws_close(code: int) -> ConnectionClosedError:
+    return ConnectionClosedError(Close(code, "scripted"), None)
+
+
[email protected](
+    ("error", "expected"),
+    [
+        pytest.param(_http_error(403), True, id="http-403"),
+        pytest.param(_http_error(404), True, id="http-404"),
+        pytest.param(_http_error(408), False, id="http-408"),
+        pytest.param(_http_error(429), False, id="http-429"),
+        pytest.param(_http_error(503), False, id="http-503"),
+        pytest.param(_ws_close(4404), True, id="ws-close-4404"),
+        pytest.param(_ws_close(4422), True, id="ws-close-4422"),
+        pytest.param(_ws_close(4429), False, id="ws-close-4429"),
+        pytest.param(_ws_close(4503), False, id="ws-close-4503"),
+        pytest.param(_ws_close(1011), False, id="ws-close-1011"),
+        pytest.param(
+            InvalidStatus(Response(403, "Forbidden", Headers())),
+            True,
+            id="ws-upgrade-403",
+        ),
+        pytest.param(RuntimeError("network down"), False, id="no-status"),
+        pytest.param(None, False, id="none"),
+    ],
+)
+def test_is_non_retryable_stream_error(
+    error: BaseException | None, expected: bool
+) -> None:
+    assert is_non_retryable_stream_error(error) is expected
+
+
+def test_stream_error_frame_reads_only_unsolicited_error_frames() -> None:
+    frame = {
+        "type": "error",
+        "id": None,
+        "error": "unknown_error",
+        "message": "Thread event stream failed.",
+    }
+
+    assert (
+        str(stream_error_frame(frame))
+        == "Protocol error [unknown_error]: Thread event stream failed."
+    )
+    assert stream_error_frame({**frame, "id": 3}) is None
+    assert stream_error_frame({"type": "event", "method": "values"}) is None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langgraph_sdk-0.4.7/tests/streaming/test_sync_thread_stream.py 
new/langgraph_sdk-0.4.8/tests/streaming/test_sync_thread_stream.py
--- old/langgraph_sdk-0.4.7/tests/streaming/test_sync_thread_stream.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/tests/streaming/test_sync_thread_stream.py  
2020-02-02 01:00:00.000000000 +0100
@@ -35,6 +35,7 @@
     message_text_delta_event,
     message_text_finish_event,
     tasks_start_event,
+    thread_stream_failed_frame,
     tool_finished_event,
     tool_started_event,
     updates_event,
@@ -868,3 +869,196 @@
             ]
     assert {"scope": "root"} in checkpoints
     assert {"scope": "child"} not in checkpoints
+
+
+def test_sync_lifecycle_clean_eof_fails_the_run_instead_of_blocking():
+    fake = SyncFakeServer()
+    fake.script([lifecycle_started_event(seq=0)])
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = run_done.result(timeout=2.0)
+
+    assert terminal.status == "errored"
+    assert "ended before terminal" in str(terminal.error)
+
+
+def test_sync_lifecycle_error_frame_becomes_the_run_error():
+    fake = SyncFakeServer()
+    fake.script([lifecycle_event(seq=0, phase="running"), 
thread_stream_failed_frame()])
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = run_done.result(timeout=2.0)
+
+    assert terminal.status == "errored"
+    assert (
+        str(terminal.error)
+        == "Protocol error [unknown_error]: Thread event stream failed."
+    )
+
+
+def test_sync_lifecycle_watcher_stops_on_a_404_without_retrying():
+    fake = SyncFakeServer()
+    fake.script_sequence([SyncStreamScript(events=[], status=404)])
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = run_done.result(timeout=2.0)
+
+    assert isinstance(terminal.error, httpx.HTTPStatusError)
+    assert terminal.error.response.status_code == 404
+    assert len(fake.stream_request_bodies) == 1
+
+
+def test_sync_minted_thread_opens_the_lifecycle_stream_after_run_start():
+    fake = SyncFakeServer()
+    fake.script([lifecycle_completed_event(seq=1)])
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(assistant_id="agent") as thread:
+            assert thread._lifecycle_watcher_thread is None
+            thread.run.start(input={})
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = run_done.result(timeout=2.0)
+
+    assert terminal.status == "completed"
+
+
+def _sync_new_thread_scripts() -> list[SyncStreamScript]:
+    return [
+        SyncStreamScript(events=[], status=404),
+        SyncStreamScript(events=[lifecycle_completed_event(seq=1)]),
+    ]
+
+
+def test_sync_run_start_restarts_a_watcher_stopped_by_a_404_for_a_new_thread():
+    fake = SyncFakeServer()
+    fake.script_sequence(_sync_new_thread_scripts())
+    fake.set_state({"ok": True})
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(thread_id="t-new", assistant_id="agent") as thread:
+            rejected = thread._run_done
+            assert rejected is not None
+            rejected.result(timeout=2.0)
+            thread.run.start(input={})
+            output = thread.output
+
+    assert output == {"ok": True}
+    assert len(fake.stream_request_bodies) == 2
+
+
+def test_sync_lifecycle_watcher_retries_a_404_that_arrives_after_run_start():
+    fake = SyncFakeServer()
+    fake.script_sequence(_sync_new_thread_scripts())
+    fake.set_state({"ok": True})
+    stream_opened = threading.Event()
+    run_started = threading.Event()
+
+    def handle(request: httpx.Request) -> httpx.Response:
+        if (
+            request.url.path.endswith("/stream/events")
+            and not fake.stream_request_bodies
+        ):
+            stream_opened.set()
+            run_started.wait(timeout=2.0)
+        return fake._handle(request)
+
+    with httpx.Client(
+        transport=httpx.MockTransport(handle), base_url="http://test";
+    ) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(thread_id="t-new", assistant_id="agent") as thread:
+            assert stream_opened.wait(timeout=2.0)
+            thread.run.start(input={})
+            run_started.set()
+            output = thread.output
+
+    assert output == {"ok": True}
+    assert len(fake.stream_request_bodies) == 2
+
+
+def 
test_sync_lifecycle_watcher_reports_the_error_frame_when_reconnects_run_out():
+    fake = SyncFakeServer()
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        stream = threads.stream(thread_id="t-1", assistant_id="agent")
+        stream._lifecycle_max_reconnect_attempts = 1
+        fake.script_sequence(
+            [
+                SyncStreamScript(events=[thread_stream_failed_frame()], 
fail_after=1)
+                for _ in range(stream._lifecycle_max_reconnect_attempts + 1)
+            ]
+        )
+        with stream as thread:
+            run_done = thread._run_done
+            assert run_done is not None
+            terminal = run_done.result(timeout=2.0)
+
+    assert (
+        str(terminal.error)
+        == "Protocol error [unknown_error]: Thread event stream failed."
+    )
+
+
+def test_sync_concurrent_run_starts_start_one_lifecycle_watcher(monkeypatch):
+    real_thread = threading.Thread
+    created_watchers: list[str] = []
+
+    class _SlowToCreateThread(real_thread):
+        def __init__(self, *args: Any, **kwargs: Any) -> None:
+            if kwargs.get("name") == "langgraph-sdk-sync-lifecycle":
+                created_watchers.append(kwargs["name"])
+                time.sleep(0.05)
+            super().__init__(*args, **kwargs)
+
+    monkeypatch.setattr(threading, "Thread", _SlowToCreateThread)
+    fake = SyncFakeServer()
+    fake.script([lifecycle_completed_event(seq=1)])
+    with httpx.Client(transport=fake.transport, base_url="http://test";) as raw:
+        threads = SyncThreadsClient(SyncHttpClient(raw))
+        with threads.stream(assistant_id="agent") as thread:
+            barrier = threading.Barrier(2)
+
+            def start_run() -> None:
+                barrier.wait()
+                thread.run.start(input={})
+
+            runners = [real_thread(target=start_run) for _ in range(2)]
+            for runner in runners:
+                runner.start()
+            for runner in runners:
+                runner.join()
+
+    assert len(created_watchers) == 1
+
+
+def test_sync_reconnect_stops_on_a_404(monkeypatch):
+    monkeypatch.setattr(_ctrl_mod.time, "sleep", lambda _delay: None)
+    opened: list[dict[str, Any]] = []
+
+    class _NotFoundTransport(SyncProtocolSseTransport):
+        def open_event_stream(self, params: dict) -> SyncEventStreamHandle:
+            opened.append(params)
+            request = httpx.Request("POST", 
"http://test/threads/t-1/stream/events";)
+            raise httpx.HTTPStatusError(
+                "not found",
+                request=request,
+                response=httpx.Response(404, request=request),
+            )
+
+    with httpx.Client(base_url="http://test";) as raw:
+        transport = _NotFoundTransport(client=raw, thread_id="t-1")
+        controller = SyncStreamController(transport, max_reconnect_attempts=5)
+        controller._shared_stream_filter = {"channels": ["values"]}
+        assert controller._reconnect_shared_stream() is False
+
+    assert len(opened) == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langgraph_sdk-0.4.7/uv.lock 
new/langgraph_sdk-0.4.8/uv.lock
--- old/langgraph_sdk-0.4.7/uv.lock     2020-02-02 01:00:00.000000000 +0100
+++ new/langgraph_sdk-0.4.8/uv.lock     2020-02-02 01:00:00.000000000 +0100
@@ -301,7 +301,7 @@
 
 [[package]]
 name = "langgraph"
-version = "1.2.15"
+version = "1.2.16"
 source = { editable = "../langgraph" }
 dependencies = [
     { name = "langchain-core" },
@@ -433,7 +433,7 @@
 
 [[package]]
 name = "langgraph-prebuilt"
-version = "1.1.0"
+version = "1.1.1"
 source = { editable = "../prebuilt" }
 dependencies = [
     { name = "langchain-core" },

Reply via email to