A LangGraph run can complete successfully while the UI never shows the final step. A reconnect can also display the same event twice. Durable execution and durable event delivery are different problems.
LangGraph 1.2.19 and Python SDK 0.4.11 were published on October 30 as separate releases pointing at the same commit, 5bd231c. The 1.2.19 notes and SDK 0.4.11 notes show that this batch is less about new graph features than about making a stream interpretable after a disconnect.
1.2.19 gives nested events a namespace
The 1.2.19 list includes changes that give messages and tool events the namespace of their graph, emit node-output messages from a graph streamed inside a node, and respect subgraphs=False in RemoteGraph.stream under a streaming parent. It also stops reporting a paused tool as failed, returns an awaited task's value while streaming, and lists ToolNode tool names in graph node metadata.
These changes share a direction. As graphs nest and tool calls multiply, a consumer needs to know which graph produced an event. Without that context, reconstructing the stream is ambiguous. The release notes alone still do not prove exactly-once delivery.
SDK 0.4.11: cursor, chunks and reconnect backoff
The Python SDK changes are more specific:
| Change | Pull request | What it addresses |
|---|---|---|
| Preserve SSE data and cursor state across chunks | #9477 | A line split across reads no longer loses its event or resume point |
Reset the reconnect cursor on every empty id |
#9440 | Includes a Last-Event-ID supplied by the caller |
| Retry failed stream reconnects with backoff | #9462 | 429, 5xx and transport errors do not become a tight retry loop |
Retry runs.wait, runs.join and runs.cancel(wait=True) reconnects with backoff |
#9481 | The policy also covers waiting APIs outside the stream |
Warn when a stream reconnects without Last-Event-ID |
#9482 | A client no longer silently resumes from an unknown position |
In SSE, id is both an event identifier and a reconnect cursor. After a disconnect, a client can send Last-Event-ID so the server resumes after that point. Without a cursor, the client does not know where to continue. The SDK now surfaces that condition instead of hiding it.
Running 0.4.10 and 0.4.11 with the same input
To see the difference, CodeBridge installed langgraph-sdk 0.4.10 and 0.4.11 in separate Python 3.13 environments and ran the minimal reproduction from issue #9299. No server or model was called; the script uses only the SSE decoder.
from langgraph_sdk.sse import SSEDecoder
decoder = SSEDecoder()
for line in (b"id: 42", b"event: values", b"data: {}", b"", b": keepalive", b""):
event = decoder.decode(line)
if event is not None:
print(event)
Actual output from 0.4.10:
StreamPart(event='values', data={}, id='42')
StreamPart(event='', data=None, id='42')
The second line is a phantom event created from a comment-only heartbeat with no data. The decoder had seen id: 42, so a later empty block inherited that state and was dispatched. The same script on 0.4.11 produced:
StreamPart(event='values', data={}, id='42')
The heartbeat no longer creates an empty event. We then checked whether an empty id resets the cursor.
decoder = SSEDecoder()
for line in (b"id: 42", b"event: values", b"data: {}", b"",
b"id", b"event: values", b"data: {}", b""):
event = decoder.decode(line)
if event is not None:
print(event)
Output from 0.4.11:
StreamPart(event='values', data={}, id='42')
StreamPart(event='values', data={}, id=None)
The second event has id=None, meaning the empty id field cleared the previous cursor instead of inheriting it. That distinction matters when a server explicitly says there is no resume ID, as opposed to accidentally omitting one.
This experiment covers only the SDK's SSE decoder. It does not verify server retention, network failures, checkpoint recovery or duplicate suppression.
Do not put execution success and delivery success in the same row
A checkpoint or final state can establish that a run finished. Whether a UI saw that fact is a property of the event stream. Separating the two makes incident analysis easier.
| Question | Where to check | What this release does not guarantee |
|---|---|---|
| Did the work finish? | Run state, checkpoint, final output | That the last event reached the screen |
| Where should we resume? | SSE id, Last-Event-ID |
That the server retains every event |
| Did we receive the same event twice? | Consumer-side ID deduplication | Exactly-once transport |
| Is reconnect retrying responsibly? | Backoff logs, 429/5xx rates | That every reconnect succeeds |
| Did we miss intermediate events? | Compare checkpoint and event log | That final state alone reconstructs the path |
Upgrading to 0.4.11 therefore does not "solve reconnects" in general. It preserves a resume point, warns when one is absent, and spaces out retries. Retention policy and duplicate semantics remain application decisions.
Tests to add before relying on it
- Disconnect mid-stream, reconnect with the last received ID, and check for missing events.
- Inject the same event ID twice and verify that the consumer does not display it twice.
- Send an empty
id, reconnect, and confirm that the old cursor is not reused. - Put a proxy returning 429 and 5xx in front of the stream; verify increasing delays and a maximum attempt count.
- Reconnect without
Last-Event-IDand confirm that a warning reaches the logs. - Simulate a successful run with a lost final event; verify how the UI reconciles its state.
On the consumer side, deduplicate by event ID and mark events without an ID as a non-resumable interval. Do not use the UI's event list as the source of truth for execution; use the checkpoint or final state.
For background on the graph model, see our graph engineering introduction and loop engineering article. This release concerns the operational layer above that structure: how a disconnected stream is read again.
The paid course below covers agent execution, verification and graph design. It does not teach LangGraph 0.4.11 reconnects or reproduce this experiment, and the link is promotional.