[GH-ISSUE #3414] [langchain]: handleEdit with { ...msg, content } breaks branch switcher due to ID collision in getMessagesMetadataMap #2731

Open
opened 2026-06-05 17:26:32 -04:00 by yindo · 0 comments
Owner

Originally created by @Jourdelune on GitHub (Apr 2, 2026).
Original GitHub issue: https://github.com/langchain-ai/docs/issues/3414

Type of issue

issue / bug

Language

N/A

Description

Summary

When implementing the branching chat pattern with @langchain/vue, following the documented handleEdit implementation
causes all branch switchers to disappear after editing a message. The root cause is an ID collision between the edited message and the original in getMessagesMetadataMap's findLast lookup.

Environment

  • @langchain/vue (latest)
  • @langchain/langgraph-sdk (latest)
  • fetchStateHistory: { limit: N } enabled

Steps to Reproduce

  1. Start a new conversation and send a message — AI responds
  2. Regenerate the AI response → branch switcher appears (1/2 · 2/2)
  3. Edit the original human message following the documented pattern:
// As documented
stream.submit(
  { messages: [{ ...msg, content: newText }] }, // keeps original id
  { checkpoint }
);
  1. Observe: after the edit completes, all branch switchers disappear — including the one created by the regen in step 2

Root Cause

The documented pattern spreads the original message object, preserving its id:

{ messages: [{ ...originalMsg, content: newText }] }

getMessagesMetadataMap in branching.js uses findLast to locate the oldest history state containing each message id:

// branching.js
const firstSeenState = findLast(options.history ?? [], (state) =>
  state.values != null &&
  options.getMessages(state.values)
    .map((m, idx) => m.id ?? idx)
    .includes(messageId)
);
const checkpointId = firstSeenState?.checkpoint?.checkpoint_id;
let branch = checkpointId != null
  ? options.branchContext.branchByCheckpoint[checkpointId]
  : void 0;

Since the edited message shares its id with the original, findLast returns the oldest state containing that id which is a checkpoint on the original branch, not the edit branch. That
checkpoint is absent from the edit branch's branchByCheckpoint map, so branch resolves to undefined → branchOptions is never set → no branch switcher is rendered.

Workaround

Submit the edited message without the original id so that LangGraph generates a new one, making findLast correctly resolve to the edit branch's checkpoint:

stream.submit(
  { messages: [{ role: "human", content: newText }] }, // ← no id
  { checkpoint }
);

Expected Behavior

The documented pattern { ...msg, content: newText } should either:

  • work correctly (fix getMessagesMetadataMap to resolve firstSeenState relative to the current branch context rather than the global history), or
  • be updated in the documentation to omit the message id

Additional Notes

A related issue: when submitting a normal new message (not an edit) from a non-default branch without passing an explicit checkpoint, submitDirect resets this.#branch = ""
(orchestrator.js:634). After stream completion getBranchView("") selects the newest fork branch rather than the one the user was viewing, causing the new response to disappear. Fix: pass
history.value.at(-1)?.checkpoint explicitly on every submit to preserve the active branch.

Originally created by @Jourdelune on GitHub (Apr 2, 2026). Original GitHub issue: https://github.com/langchain-ai/docs/issues/3414 ### Type of issue issue / bug ### Language N/A ### Description ## Summary When implementing the [branching chat pattern](https://langchain-ai.github.io/langgraphjs/how-tos/branching-chat/) with `@langchain/vue`, following the documented `handleEdit` implementation causes all branch switchers to disappear after editing a message. The root cause is an ID collision between the edited message and the original in `getMessagesMetadataMap`'s `findLast` lookup. ## Environment - `@langchain/vue` (latest) - `@langchain/langgraph-sdk` (latest) - `fetchStateHistory: { limit: N }` enabled ## Steps to Reproduce 1. Start a new conversation and send a message — AI responds 2. Regenerate the AI response → branch switcher appears (1/2 · 2/2) 3. Edit the original human message following the documented pattern: ```ts // As documented stream.submit( { messages: [{ ...msg, content: newText }] }, // keeps original id { checkpoint } ); ``` 4. Observe: after the edit completes, all branch switchers disappear — including the one created by the regen in step 2 ## Root Cause The documented pattern spreads the original message object, preserving its id: ```ts { messages: [{ ...originalMsg, content: newText }] } ``` `getMessagesMetadataMap` in branching.js uses findLast to locate the oldest history state containing each message id: ```ts // branching.js const firstSeenState = findLast(options.history ?? [], (state) => state.values != null && options.getMessages(state.values) .map((m, idx) => m.id ?? idx) .includes(messageId) ); const checkpointId = firstSeenState?.checkpoint?.checkpoint_id; let branch = checkpointId != null ? options.branchContext.branchByCheckpoint[checkpointId] : void 0; ``` Since the edited message shares its id with the original, findLast returns the oldest state containing that id which is a checkpoint on the original branch, not the edit branch. That checkpoint is absent from the edit branch's branchByCheckpoint map, so branch resolves to undefined → branchOptions is never set → no branch switcher is rendered. ## Workaround Submit the edited message without the original id so that LangGraph generates a new one, making findLast correctly resolve to the edit branch's checkpoint: ```ts stream.submit( { messages: [{ role: "human", content: newText }] }, // ← no id { checkpoint } ); ``` ## Expected Behavior The documented pattern { ...msg, content: newText } should either: - work correctly (fix getMessagesMetadataMap to resolve firstSeenState relative to the current branch context rather than the global history), or - be updated in the documentation to omit the message id ## Additional Notes A related issue: when submitting a normal new message (not an edit) from a non-default branch without passing an explicit checkpoint, submitDirect resets this.#branch = "" (orchestrator.js:634). After stream completion getBranchView("") selects the newest fork branch rather than the one the user was viewing, causing the new response to disappear. Fix: pass history.value.at(-1)?.checkpoint explicitly on every submit to preserve the active branch.
yindo added the langchainexternal labels 2026-06-05 17:26:32 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/docs#2731