Skip to content

BinaryOperatorAggregate stores the Overwrite wrapper itself when it is the first write to an empty channel #9017

Closed as duplicate

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 langgraph.channels.binop import BinaryOperatorAggregate
from langgraph.types import Overwrite

ch = BinaryOperatorAggregate(dict | None, lambda existing, new: new)
ch.key = "goal"
ch.update([Overwrite({"goal": "ship"})])
value = ch.get()
print(type(value).__name__, value)
# Overwrite Overwrite(value={'goal': 'ship'}, type='__overwrite__')

Description

  • System information
    • LangGraph main at 230927fb3a9ac9b2893a30322b4dfea7cdea9a8f (the update implementation is unchanged in the latest release)

When the first write to an empty BinaryOperatorAggregate channel is an Overwrite, the empty-channel branch assigns values[0] verbatim, so the channel value — and the checkpointed state — holds the Overwrite wrapper itself instead of its payload. Every consumer that later reads the state then sees the wrapper object, typically failing downstream with something like TypeError: 'Overwrite' object is not subscriptable.

Union-typed channels (e.g. Annotated[dict | None, reducer]) have no constructible default, so they start out MISSING; a replace-style write into a fresh thread or a never-written channel is exactly this path.

The same write is handled correctly once the channel has a value:

ch = BinaryOperatorAggregate(dict | None, lambda existing, new: new).from_checkpoint({"seed": True})
ch.update([Overwrite({"goal": "ship"})])
ch.get()   # {'goal': 'ship'}  — unwrapped, as expected

DeltaChannel.update also unwraps in the same situation, so only the empty-channel first-write branch of BinaryOperatorAggregate skips the _get_overwrite check. Expected: Overwrite(x) as a first write stores x, matching the seeded-channel behavior.

Error Message and Stack Trace (if applicable)

The wrapper silently persists into the checkpointed state; the failure surfaces later at the consumer, e.g. TypeError: 'Overwrite' object is not subscriptable when the persisted state is read back.

Activity

  1. leilei3167 commented on Sep 21, 2026

    @leilei3167

    I reproduced this on current main: when a BinaryOperatorAggregate channel is still empty, the first write that wraps the value in Overwrite stores the wrapper object itself. Seeded / later paths already unwrap via _get_overwrite, so consumers later hit a TypeError when they expect the bare value.

    Proposed fix: in libs/langgraph/langgraph/channels/binop.py, unwrap Overwrite on that empty-channel first write the same way the seeded path does, plus a small test_channels regression covering empty vs seeded first writes.

    Could a maintainer please assign this issue to me (leilei3167) so I can open a PR under the repo process? Happy to take it from there.

  2. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    I'd like to fix this one. Confirmed on main: BinaryOperatorAggregate.update seeds the channel with �alues[0] verbatim when the channel is empty, so a first-write Overwrite stores the wrapper object itself. Fix mirrors the single-value-channel unwrap in #9019 (_get_overwrite on the seed), with a regression test. Could a maintainer assign this to me so I can open the PR?

  3. Rainmemery commented on Sep 21, 2026

    @Rainmemery

    PR opened: #9037 - BinaryOperatorAggregate.update now unwraps an Overwrite seed via _get_overwrite on the empty-channel branch (mirroring the #9019 single-value fix), with a regression test asserting the channel holds the payload dict, not the wrapper. Existing est_channels.py all pass.

  4. Rainmemery commented on Sep 26, 2026

    @Rainmemery

    Sydney Runkle (@sydney-runkle) could you assign this one too? My PR #9037 was auto-closed pending assignment; once assigned the bot will reopen it automatically.

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