Skip to content

Scalar resume is accepted for multiple interrupts in one subgraph task #8579

Description

@russeell

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used GitHub search to find a similar issue or pull request and did not find one.
  • I am sure this is a bug in LangGraph rather than application code.
  • The bug reproduces on the latest main branch.
  • This is not related to the langchain-community package.
  • The example below is self-contained and can be run as-is.

Related Issues / PRs

Reproduction Steps / Example Code (Python)

import operator
from typing import Annotated

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import START, StateGraph
from langgraph.types import Command, interrupt
from typing_extensions import TypedDict


class State(TypedDict):
    answers: Annotated[list[str], operator.add]


def ask_left(state: State) -> State:
    return {"answers": [f"left={interrupt('left')}"]}


def ask_right(state: State) -> State:
    return {"answers": [f"right={interrupt('right')}"]}


child = (
    StateGraph(State)
    .add_node("ask_left", ask_left)
    .add_node("ask_right", ask_right)
    .add_edge(START, "ask_left")
    .add_edge(START, "ask_right")
    .compile()
)

graph = (
    StateGraph(State)
    .add_node("child", child)
    .add_edge(START, "child")
    .compile(checkpointer=InMemorySaver())
)
config = {"configurable": {"thread_id": "nested-parallel-interrupts"}}

first = graph.invoke({"answers": []}, config)
print([(item.id, item.value) for item in first["__interrupt__"]])

# This should raise because two interrupts are pending.
second = graph.invoke(Command(resume="ambiguous"), config)
print(second)

Error Message and Stack Trace (if applicable)

Two different interrupt IDs are initially returned, but no RuntimeError is raised.
The scalar value is delivered to one child branch and the other interrupt remains pending:

{'answers': [], '__interrupt__': [Interrupt(value='right', id='...')]}

Description

PR #6108 intentionally rejects Command(resume=<scalar>) when multiple interrupts are pending because their ordering is nondeterministic. That validation works when each interrupt belongs to a separate top-level task, but it is missed when one parent task wraps a subgraph containing parallel interrupting nodes.

The parent task persists both child interrupts in a single INTERRUPT pending write. PregelLoop._pending_interrupts() currently records only value[0].id for each task, so two child interrupts grouped under one task are counted as one. A scalar resume is then accepted and delivered to one branch according to internal execution order rather than an explicit interrupt ID.

I expected the same RuntimeError used for other multiple-pending-interrupt cases, requiring an interrupt_id -> resume value mapping. This matters for concurrent approval or human-input flows because a scalar response can otherwise be associated with an unintended request.

A narrow fix would collect every interrupt ID in each task's INTERRUPT write while retaining the existing task-level deduplication for resolved writes. I would be happy to submit the fix with sync and async regression coverage if a maintainer approves the approach and assigns this issue to me.

System Info

OS: Darwin (arm64)
Python: 3.13.13
LangGraph: current main at d56666f
langgraph: 1.2.10
langgraph-checkpoint: 4.2.0
langchain-core: 1.5.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions