For AI agents: a documentation index is available at https://www.mongodb.com/docs/llms.txt — markdown versions of all pages are available by appending .md to any URL path.
Docs Menu

Session fork vs LangGraph time-travel

The Python SDK has a matching session fork guide <../../../../python/packages/agent-engine-sdk-langgraph/docs/session-fork.md>`__. LangGraph.js time-travel stays on one thread: ``graph.updateState and a historical checkpoint_id write the next checkpoint on that same thread_id. The conversation you patched is the conversation you keep using.

The platform treats a conversation as a session. A fork is therefore a new session, not a write onto the source conversation. Native-checkpoint sessions copy the selected checkpoint into a new LangGraph thread. Durable workflow sessions submit the selected committed step and its reconstructed state to OE, which creates a dormant session branch and validates the state against the committed marker. Both paths leave the source unchanged.

forkNativeSession creates the native branch session (CreateBranch), copies the selected completed root checkpoint onto the empty dest thread, and applies an optional dest updateState patch. The durable path (forkDurableSession) creates an OE-owned branch directly from fenced scratch. Both are used by:

  1. In-app graph.updateState (wrapped when the LangGraphBaseAgent is constructed — LangGraph.js has no synchronous update surface, so only the async method is wrapped)

  2. The LangGraphForkPlugin AER write plugin (HTTP registration is a follow-up)

Empty-thread native updateState remains ordinary checkpoint initialization. A native dest patch after copy is not intercepted either: dest config has no source request_context.

In-app updateState fork uses the default session_id:workspace_id thread key so the destination session_id is recoverable. Custom App.resolveThreadId is rejected on that path; use the fork plugin when a custom key is required.

The branch key is deterministic: the same source history, state patch, and node always map to the same branch, so retries reuse the OE branch instead of creating duplicate sessions. The selected completed root checkpoint, its replay plan, and a dest patch’s target node are validated before CreateBranch, so an unusable target fails before a branch exists.

Dest must be empty: copy, then optional dest patch. If dest is already occupied, copy fails, or the patch fails, the fork raises and dest is left as-is. Fork again into a new session rather than recovering that dest.

Public fork does not take asNode. If a dest state patch omits it, a single-node graph fills it in; a multi-node graph, or an explicit node the graph does not have, fails before any branch is created instead of guessing.

Within a fenced durable attempt, getStateHistory iterates only that attempt’s scratch checkpoints (read-only). Scratch remains temporary and request-scoped; OE activity history and committed step markers remain the durable authority. Calling updateState(config, null) on a selected root-loop checkpoint reconstructs application state from scratch and posts its OE step ordinal and state to CreateBranch. OE uses its own issued tenant, workspace, session, and execution identity and rejects a state whose hash does not match the selected marker.

PlatformCheckpointer records the absolute OE ordinal in each committed root scratch checkpoint’s metadata. LangGraph’s relative step value is not used for this mapping because its offset changes when a new attempt is seeded from previous session state. On a branch attempt, OE initializes the step counter after branch_lineage.source_step_ordinal, so the recorded value already includes the inherited cutoff.

The returned config identifies the new session’s thread coordinate. It does not carry the source RequestContext; the future branch invocation receives its own request-scoped context. No LangGraph checkpoint is copied: the durable runtime seeds a fresh attempt from the branch state when that session is first invoked. A non-empty updateState patch is rejected at the caller boundary before branch creation because the current OE contract binds the exact source state; applying a patch as a branch-local transition is separate work.

After a native fork, dest already holds the copied conversation. The platform then invokes dest. The payload often has message="" because there is no new user turn yet.

NativeSession would otherwise wrap that as an empty HumanMessage and append an empty user message onto the copy. For the dest thread of a fork that just happened in this process, the first invoke consumes a one-shot mark (takeContinueWithoutUserMessage): a blank payload becomes invoke(null) so the graph continues from the copied checkpoint. A non-blank first dest turn still consumes the mark so a later empty message is a real empty user turn.

A dest execution that starts in a new process does not see the mark; that invoke uses whatever payload OE sends. Unused marks are a bounded LRU in this process; an evicted dest behaves the same as that new-process case.

Rate this page