Skip to content

update_state with Overwrite on a plain key persists the Overwrite wrapper instead of its payload (LastValue / LastValueAfterFinish / UntrackedValue) #9019

Open

Description

@BlueX888

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Reproduction Steps / Example Code (Python)

from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Overwrite

class S(TypedDict):
    x: str

g = StateGraph(S)
g.add_node("n", lambda s: {"x": "from_node"})
g.add_edge(START, "n")
g.add_edge("n", END)
graph = g.compile(checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": "t1"}}
graph.invoke({"x": "init"}, cfg)

graph.update_state(cfg, {"x": Overwrite("replaced")})
print(graph.get_state(cfg).values)
# {'x': Overwrite(value='replaced', type='__overwrite__')}   <- the wrapper itself

Description

  • System information
    • LangGraph main at 230927fb3a9ac9b2893a30322b4dfea7cdea9a8f

A replace-style state update (update_state with an Overwrite, i.e. the form used for replace-writes through the API server and by clients that wrap replace-style updates in Overwrite) on a plain (non-annotated) state key persists the Overwrite wrapper object into the checkpointed state instead of its payload. get_state().values then returns the wrapper, and any downstream consumer of the state fails (e.g. TypeError: 'Overwrite' object is not subscriptable).

Root cause: plain keys compile to LastValue, and LastValue.update (also LastValueAfterFinish.update and UntrackedValue.update) assigns values[-1] verbatim without the _get_overwrite check. BinaryOperatorAggregate (on its seeded path) and DeltaChannel both unwrap an Overwrite write, so the single-value channels are the inconsistent ones.

Expected: update_state(cfg, {"x": Overwrite("replaced")}) stores "replaced".

Channel-level repro for each affected class:

from langgraph.channels.last_value import LastValue
from langgraph.types import Overwrite

ch = LastValue(dict)
ch.key = "x"
ch.update([Overwrite({"a": 1})])
ch.get()   # Overwrite(value={'a': 1}, type='__overwrite__') instead of {'a': 1}

Error Message and Stack Trace (if applicable)

No error is raised at update time; the wrapper silently persists into state and surfaces at the consumer, e.g. TypeError: 'Overwrite' object is not subscriptable.

Activity

  1. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    I'd like to fix this. The root cause is clear: LastValue.update, LastValueAfterFinish.update, and UntrackedValue.update assign values[-1] verbatim without running it through _get_overwrite, unlike BinaryOperatorAggregate (on its seeded path) and DeltaChannel, which already unwrap an Overwrite write. The fix is to unwrap through _get_overwrite in those three single-value channels, so a replace-style update_state persists the payload (e.g. "replaced") instead of the Overwrite wrapper. I'll add channel-level regression tests for all three plus an update_state end-to-end test.

    Could you assign me to #9019? Thanks!

  2. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    The fix is ready: unwrap Overwrite writes in LastValue.update, LastValueAfterFinish.update, and UntrackedValue.update via _get_overwrite (mirroring BinaryOperatorAggregate / DeltaChannel), with regression tests covering overwrite-on-plain-key with LastValue, LastValueAfterFinish, and UntrackedValue. All tests pass locally. May I get this assigned so I can open the PR?

  3. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    PR opened: #9032 — LastValue / LastValueAfterFinish / UntrackedValue now unwrap an Overwrite write through _get_overwrite (mirroring BinaryOperatorAggregate and DeltaChannel), with regression tests for all three channels. Happy to adjust after review.

  4. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    cc maintainers (LangChain (@langchain-ai)) - could one of you assign me this issue? PR #9032 was auto-closed because external contributors need to be assigned first. The fix (unwrap Overwrite writes in the single-value channels + regression tests) is ready; once assigned the PR will be reopened automatically.

  5. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    Sydney Runkle (@sydney-runkle) - could you assign me this issue (or another maintainer)? My PR #9032 (LastValue/LastValueAfterFinish/UntrackedValue unwrap Overwrite writes via _get_overwrite, with regression tests) was auto-closed pending assignment; once assigned the bot will reopen it automatically. Happy to adjust anything after review.

  6. Iyanuoluwa007 commented on Oct 21, 2026

    @Iyanuoluwa007

    Confirmed on current main (3cd57c0): LastValue, LastValueAfterFinish and UntrackedValue all store an Overwrite write verbatim, and the repro persists the wrapper. Unwrapping with _get_overwrite, as BinaryOperatorAggregate and DeltaChannel do, fixes it without other test changes in the langgraph suite.

    Rain. (@Rainmemery) has this in #9032 (auto-closed only for the missing assignment), so assigning them seems simplest. One thing worth a maintainer's view: _get_overwrite also unwraps the {"__overwrite__": v} and {"type": "__overwrite__", "value": v} dict forms, so a plain key that stores exactly one of those dicts would now be unwrapped. That matches reducer channels, but it's a small behaviour change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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