[PR #74] [MERGED] fix: concurrent graph update issue (Issues #52 + 65) #100

Closed
opened 2026-02-16 06:17:09 -05:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/deepagentsjs/pull/74
Author: @brettshollenberger
Created: 12/5/2025
Status: Merged
Merged: 12/11/2025
Merged by: @dqbd

Base: mainHead: fix-concurrent-graph-update


📝 Commits (2)

📊 Changes

2 files changed (+20 additions, -9 deletions)

View changed files

.changeset/fair-hounds-make.md (+5 -0)
📝 src/middleware/fs.ts (+15 -9)

📄 Description

Fixes Issues

Why

This was a sneaky bug. It's gonna be a long walk so bear with me.

The files channel defines a reducer via withLangGraph(), so why are all these bugs complaining about the LastValue reducer being used? LastValue is only supposed to be a fallback when no reducer is defined.

  const meta = schemaMetaRegistry.get(zodSchemaObject);
  if (meta?.reducer) {
    return new BinaryOperatorAggregate(meta.reducer);  // Uses your reducer
  } else {
    return new LastValue();  // Fallback - only one value per step allowed
  }

But we DO have a reducer!

  const FilesystemStateSchema = z3.object({
    files: withLangGraph(z3.record(...), {
      reducer: { fn: fileDataReducer, ... }  // Right here! We have a reducer!
    }),
  });

So what's going on?

Reducer Registration Issue

LangGraph stores reducers in schemaMetaRegistry. When you call withLangGraph(), it does:

  schemaMetaRegistry.set(zodSchemaObject, { reducer: yourReducer });

The critical thing here: the WeakMap is keyed by object identity. It's not checking if the schemas are structurally equivalent - it's checking if they're the exact same object in memory.

Problem 1: Duplicate packages

This is actually what's going on in issues 52 and 65, which covers up the true underlying issue (below)

When I personally encountered this bug, I ran:

npm ls @langchain/langgraph

And saw two different paths:

  deepagents → @langchain/langgraph@1.0.2_...3kxia2yhuhf7i35oisvqsvwple
  my-app  → @langchain/langgraph@1.0.2_...krfp3ij63gq4bje2wvj2d6skxu

Same version number, but pnpm created two separate copies because the dependency trees differed slightly. Each copy has its own schemaMetaRegistry WeakMap.

So deepagents registers the reducer in WeakMap A, but the app's graph looks it up in WeakMap B. WeakMap B is empty. No reducer found. Falls back to LastValue.

This causes parallel tool calls to fail, even though we're supposed to have a reducer

The fix for bug 1 (the bug everyone is reporting)

Add pnpm overrides to force a single copy (I found I had to pin all 3 of these to get a single, consistent schemaMetaRegistry. It's worth considering whether this singleton pattern in the core codebase might be refactored another way to avoid these kinds of issues.

  "pnpm": {
    "overrides": {
      "@langchain/langgraph": "1.0.2",
      "@langchain/core": "^1.0.0",
      "zod": "3.25.76"
    }
  }

Problem 2: A real bug in deepagents

With packages deduped, I now got a different error:

  Channel "files" already exists with a different type.

Now LangGraph sees the reducer, but it's complaining about duplicate channel definitions.

The real culprit

Look at where FilesystemStateSchema was defined:

  function createFilesystemMiddleware() {
    // This runs EVERY time the function is called
    const FilesystemStateSchema = z3.object({
      files: withLangGraph(...)
    });

    return createMiddleware({ stateSchema: FilesystemStateSchema });
  }

Every call to createFilesystemMiddleware() creates a brand new Zod object. And createDeepAgent calls it multiple times:

  1. Once for the parent agent's middleware
  2. Once for each subagent's defaultMiddleware

Each call registers a different schema object in the WeakMap. When LangGraph tries to compose the graph, it sees multiple files channels with different object identities and throws.

The fix

Move the schema to module level so it's created only once. One WeakMap entry. No conflicts. Reducer works. Parallel tools work.

TL;DR

Two bugs stacked on top of each other:

  1. pnpm created duplicate package copies → separate WeakMaps → reducer lookup failed → LastValue used instead (this is the bug people are seeing in #52 + #65 )
  2. Schema created inside function → new object each call → multiple registrations → "channel already exists" error

Both needed to be fixed, but one is an issue on the user side, and one we can fix for deepagents package itself.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/langchain-ai/deepagentsjs/pull/74 **Author:** [@brettshollenberger](https://github.com/brettshollenberger) **Created:** 12/5/2025 **Status:** ✅ Merged **Merged:** 12/11/2025 **Merged by:** [@dqbd](https://github.com/dqbd) **Base:** `main` ← **Head:** `fix-concurrent-graph-update` --- ### 📝 Commits (2) - [`79d50ca`](https://github.com/langchain-ai/deepagentsjs/commit/79d50ca730f5e9aa8ed86396af4fe1658681a970) Fix concurrent graph update issue - [`27c4211`](https://github.com/langchain-ai/deepagentsjs/commit/27c4211c1cb746ccf9c95681925f6e2988640d4b) Add changeset ### 📊 Changes **2 files changed** (+20 additions, -9 deletions) <details> <summary>View changed files</summary> ➕ `.changeset/fair-hounds-make.md` (+5 -0) 📝 `src/middleware/fs.ts` (+15 -9) </details> ### 📄 Description ## Fixes Issues * https://github.com/langchain-ai/deepagentsjs/issues/52 * https://github.com/langchain-ai/deepagentsjs/issues/65 ## Why This was a sneaky bug. It's gonna be a long walk so bear with me. The files channel defines a reducer via `withLangGraph()`, so why are all these bugs complaining about the `LastValue` reducer being used? `LastValue` is only supposed to be a fallback when no reducer is defined. ```typescript const meta = schemaMetaRegistry.get(zodSchemaObject); if (meta?.reducer) { return new BinaryOperatorAggregate(meta.reducer); // Uses your reducer } else { return new LastValue(); // Fallback - only one value per step allowed } ``` But we DO have a reducer! ```typescript const FilesystemStateSchema = z3.object({ files: withLangGraph(z3.record(...), { reducer: { fn: fileDataReducer, ... } // Right here! We have a reducer! }), }); ``` So what's going on? ### Reducer Registration Issue LangGraph stores reducers in `schemaMetaRegistry`. When you call `withLangGraph()`, it does: ```typescript schemaMetaRegistry.set(zodSchemaObject, { reducer: yourReducer }); ``` The critical thing here: the WeakMap is keyed by object identity. It's not checking if the schemas are structurally equivalent - it's checking if they're the exact same object in memory. ### Problem 1: Duplicate packages This is actually what's going on in issues 52 and 65, which covers up the true underlying issue (below) When I personally encountered this bug, I ran: ```bash npm ls @langchain/langgraph ``` And saw two different paths: ```bash deepagents → @langchain/langgraph@1.0.2_...3kxia2yhuhf7i35oisvqsvwple my-app → @langchain/langgraph@1.0.2_...krfp3ij63gq4bje2wvj2d6skxu ``` Same version number, but pnpm created two separate copies because the dependency trees differed slightly. Each copy has its own schemaMetaRegistry WeakMap. So deepagents registers the reducer in WeakMap A, but the app's graph looks it up in WeakMap B. WeakMap B is empty. No reducer found. Falls back to LastValue. **This causes parallel tool calls to fail, even though we're supposed to have a reducer** ### The fix for bug 1 (the bug everyone is reporting) Add pnpm overrides to force a single copy (I found I had to pin all 3 of these to get a single, consistent `schemaMetaRegistry`. It's worth considering whether this singleton pattern in the core codebase might be refactored another way to avoid these kinds of issues. ```json "pnpm": { "overrides": { "@langchain/langgraph": "1.0.2", "@langchain/core": "^1.0.0", "zod": "3.25.76" } } ``` ### Problem 2: A real bug in deepagents With packages deduped, I now got a different error: ```bash Channel "files" already exists with a different type. ``` Now LangGraph sees the reducer, but it's complaining about duplicate channel definitions. ### The real culprit Look at where FilesystemStateSchema was defined: ```typescript function createFilesystemMiddleware() { // This runs EVERY time the function is called const FilesystemStateSchema = z3.object({ files: withLangGraph(...) }); return createMiddleware({ stateSchema: FilesystemStateSchema }); } ``` Every call to createFilesystemMiddleware() creates a brand new Zod object. And createDeepAgent calls it multiple times: 1. Once for the parent agent's middleware 2. Once for each subagent's defaultMiddleware Each call registers a different schema object in the WeakMap. When LangGraph tries to compose the graph, it sees multiple files channels with different object identities and throws. ### The fix Move the schema to module level so it's created only once. One WeakMap entry. No conflicts. Reducer works. Parallel tools work. ### TL;DR Two bugs stacked on top of each other: 1. pnpm created duplicate package copies → separate WeakMaps → reducer lookup failed → LastValue used instead (this is the bug people are seeing in #52 + #65 ) 2. Schema created inside function → new object each call → multiple registrations → "channel already exists" error Both needed to be fixed, but one is an issue on the user side, and one we can fix for deepagents package itself. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-02-16 06:17:09 -05:00
yindo closed this issue 2026-02-16 06:17:09 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#100