Skip to content

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

@Jourdelune

Description

@Jourdelune

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    externalUser is not a member of langchain-ailangchainFor docs changes to LangChain

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions