Skip to content

fix(sdk): coalesce submit threadId when useStream hook is unbound - #2569

Open
stescott wants to merge 2 commits into
langchain-ai:mainfrom
stescott:fix/usestream-submit-respects-thread-id
Open

fix(sdk): coalesce submit threadId when useStream hook is unbound#2569
stescott wants to merge 2 commits into
langchain-ai:mainfrom
stescott:fix/usestream-submit-respects-thread-id

Conversation

@stescott

@stescott stescott commented Jun 23, 2026

Copy link
Copy Markdown

Summary

Aligns the LGP useStream / orchestrator submit() paths with the custom transport behavior: when the hook is mounted with threadId: null, a caller-provided submitOptions.threadId should be usable for runs.stream() without always going through threads.create().

Context / docs nuance

SubmitOptions.threadId is documented as:

"The ID to use when creating a new thread… when threadId is null or undefined."

So today’s LGP behavior (pass it into threads.create(), then stream on the response thread_id) is consistent with that wording. The problems are:

  1. Inconsistency — custom transport already does this.#threadId ?? submitOptions?.threadId and streams without a forced create; LGP React + orchestrator do not.
  2. Pre-provisioned threads — when the host has already created the thread (upsert/ifExists: do_nothing), submit() should not need another threads.create() just because the hook prop is still null (bootstrap gate / render lag).
  3. Fragile create path — the run uses thread.thread_id from the create response rather than the caller-supplied ID; any platform mismatch can split conversation state.

Root cause (LGP)

In stream.lgp.tsx submit():

let usableThreadId = threadId; // hook prop only

if (!usableThreadId) {
  const thread = await client.threads.create({
    threadId: submitOptions?.threadId,
    ...
  });
  usableThreadId = thread.thread_id;
}

When hook threadId is null and submitOptions.threadId is set, LGP always calls threads.create() instead of coalescing and streaming directly (custom transport pattern).

Fix

  • Coalesce threadId ?? submitOptions?.threadId before deciding to mint
  • When adopting submitOptions.threadId while the hook is still unbound, sync internal refs / onThreadId without calling threads.create()
  • Mirror the same coalesce in orchestrator.ts (LGP non-React path)

Test plan

  • New regression test: uses submitOptions.threadId when hook threadId is null (no threads.create)
  • pnpm test src/react/stream.lgp.test.tsx in libs/sdk

Related / consumer note

  • @superagent/ui-kit HOZ-3555: consumer-side fix gates useStream until bootstrap completes, awaits upsert, and pins threadId on submit. That works on current SDK when create honors the hint; this PR makes the SDK path match custom transport and avoids redundant create for pre-provisioned threads.
  • Happy to adjust if maintainers prefer documenting “create-only” semantics and adding an explicit opt-out (e.g. ifExists) instead.

@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 24fdf8a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@langchain/langgraph-sdk Patch
@langchain/angular Patch
@langchain/react Patch
@langchain/svelte Patch
@langchain/vue Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@langchain/langgraph-checkpoint

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint@2569

@langchain/langgraph-checkpoint-mongodb

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-mongodb@2569

@langchain/langgraph-checkpoint-postgres

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-postgres@2569

@langchain/langgraph-checkpoint-redis

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-redis@2569

@langchain/langgraph-checkpoint-sqlite

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-sqlite@2569

@langchain/langgraph-checkpoint-validation

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-validation@2569

create-langgraph

npm i https://pkg.pr.new/langchain-ai/langgraphjs/create-langgraph@2569

@langchain/langgraph-api

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-api@2569

@langchain/langgraph-cli

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-cli@2569

@langchain/langgraph

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph@2569

@langchain/langgraph-cua

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-cua@2569

@langchain/langgraph-supervisor

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-supervisor@2569

@langchain/langgraph-swarm

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-swarm@2569

@langchain/langgraph-ui

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-ui@2569

@langchain/langgraph-sdk

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-sdk@2569

@langchain/angular

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/angular@2569

@langchain/react

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/react@2569

@langchain/svelte

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/svelte@2569

@langchain/vue

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/vue@2569

commit: 24fdf8a

@stescott stescott changed the title fix(sdk): honor submitOptions.threadId when useStream threadId is null fix(sdk): coalesce submit threadId when useStream hook is unbound Jun 23, 2026
@stescott
stescott marked this pull request as ready for review June 23, 2026 19:54
@stescott
stescott marked this pull request as draft June 23, 2026 21:24

@christian-bromann Christian Bromann (christian-bromann) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch on the redundant-create / orphan-thread issue. My concern is that skipping creation entirely breaks the documented contract: SubmitOptions.threadId is documented as the ID used to create a new thread (optimistic UI). After this change, an unbound hook streams straight to /threads/${threadId}/runs/stream, which 404s if the host hasn't pre-provisioned the thread.

Could we use the idempotent middle ground instead of skipping create?

let usableThreadId = threadId ?? submitOptions?.threadId;

if (!threadId) { // hook still unbound
  const thread = await client.threads.create({
    threadId: usableThreadId,   // honor caller-supplied hint
    ifExists: "do_nothing",     // idempotent for pre-provisioned threads
    metadata: submitOptions?.metadata,
    signal,
  });
  usableThreadId = usableThreadId ?? thread.thread_id;
  threadIdRef.current = usableThreadId;
  threadIdStreamingRef.current = usableThreadId;
  onThreadId(usableThreadId);
}

This hits all three goals: no redundant churn/orphan, streams on the caller ID, and still provisions for callers relying on the documented behavior.

Smaller notes:

  • Metadata drop: the adopt branch skips create, so submitOptions.metadata is never applied at the thread level. The ifExists approach avoids this.
  • Tests: only the React path is covered, not orchestrator.ts, and there's no test for the risky case (unbound hook + threadId for a thread that doesn't exist server-side).

I also want to point out that we recommend users to use @langchain/react for better streaming DevX and feature support. Eventually we will deprecate and remove the current React primitives in the SDK package.

Jay Scott and others added 2 commits August 7, 2026 10:49
When the hook is bound with threadId: null (e.g. while a host app
bootstraps thread creation), submit() incorrectly ignored
submitOptions.threadId and always called threads.create(), using
whatever thread_id the server returned — often a newly minted orphan
thread.

Align LGP useStream/orchestrator with the custom transport path:
coalesce hook threadId ?? submitOptions.threadId before minting.

Adds regression test for the HOZ-3555 DocuSign return race.

Co-authored-by: Cursor <cursoragent@cursor.com>
Address review feedback: keep SubmitOptions.threadId create semantics via
threads.create with ifExists: do_nothing instead of skipping create, so
pre-provisioned threads are reused and missing threads are still provisioned.

Co-authored-by: Cursor <cursoragent@cursor.com>
@stescott
stescott force-pushed the fix/usestream-submit-respects-thread-id branch from 86679e1 to 24fdf8a Compare August 7, 2026 17:50
@stescott
stescott marked this pull request as ready for review August 7, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants