Skip to content

AI "update" tool intermittently throws when running against a Yjs-collaborative document #2946

Description

@AntoLC

What’s broken?

The AI feature does not seems to work since the version 0.52.1 (probably 0.52.0 as well).
The problem seems to occur when a document is collaborative (with withCollaboration ).

What did you expect to happen?

The AI should work when the doc is collaborative as well.

Steps to reproduce

  1. Open a collaborative document with a single paragraph, e.g. Bonjour le monde.
  2. Select the whole paragraph.
  3. Open the AI menu and run "Improve writing" (any prompt that returns a single update operation on the selected block works).
  4. The LLM responds with a perfectly valid tool call, e.g.:
    {"type":"update","id":"<blockId>$","block":"<p>Bonjour à tous</p>"}
  5. Applying this operation intermittently throws, and the AI menu shows a generic error even though the LLM call itself succeeded (chat.status === "ready", chat.error === undefined).

BlockNote version

v0.52.1

Environment

No response

Additional context

AI Report

Observed errors

Reproduced twice from the same input/action, with two different stack traces:

A) "html diff"

ChunkExecutionError: Tool execution failed: html diff
at Object.transform (StreamToolExecutor.ts:131:15)
Caused by: Error: html diff
at Object.Pe [as rebaseTool] (rebaseTool.ts:51:15)
at Object.execute (createUpdateBlockTool.ts:205:33)
at Object.transform (StreamToolExecutor.ts:123:43)

This is the self-consistency check in createHTMLRebaseTool (src/api/formats/html-blocks/tools/rebaseTool.ts), which round-trips the current block through blocksToHTMLLossytryParseHTMLToBlocks and throws if updateToReplaceSteps finds any diff between the original and the round-tripped version.

B) "Position not found, cannot track positions"

ChunkExecutionError: Tool execution failed: Position not found, cannot track positions
at Object.transform (StreamToolExecutor.ts:131:15)
Caused by: Error: Position not found, cannot track positions
at Object.from (RelativePositionMapping.ts:63:23)
at Object.execute (createUpdateBlockTool.ts:208:64)
at Object.transform (StreamToolExecutor.ts:123:43)

This comes from RelativePositionMappingExtension.mapPosition() (@blocknote/core/src/yjs/extensions/RelativePositionMapping.ts), used by createUpdateBlockTool to track the updateSelection.from/to positions (via trackPosition()) across the async LLM round-trip. relativePositionToAbsolutePosition returns null — per the code's own comment, "This can happen if the element is garbage collected."

Analysis

createUpdateBlockTool's outer factory calls trackPosition(editor, options.updateSelection.from/to) once, at stream-tool creation time (before the LLM responds). In a Yjs-collaborative document, this resolves to a relative Yjs position anchored via RelativePositionMappingExtension. Later, inside execute() (createUpdateBlockTool.ts:207-212), the tracked position is resolved via tool.invertMap.invert().map(selectionPositions.from()).

Because the "update" operation being applied is itself replacing content within (or very near) the same selection range that was anchored, it looks like the act of applying the update can invalidate/GC the Yjs relative-position anchor it's simultaneously trying to resolve — a self-referential race. This would explain why the same input intermittently produces either the position error directly, or the "html diff" self-check tripping (likely because the document is observed mid-mutation by the rebase's clean-projection logic).

⚠️ This wouldn't show up in a non-collaborative setup, since updateSelection positions would fall back to plain ProseMirror position mapping (see the ySyncPluginState.binding.type.length === 0 fallback path in the same file), which doesn't have this GC-based failure mode.

Impact

The failure is silent/confusing from the app's perspective: the LLM call succeeds fully (chat.status: "ready", no chat.error), but the returned, well-formed tool call fails to apply, and AIExtension only logs console.warn("Error calling LLM", ...) — no user-facing detail. Since it's intermittent (same input, two different stack traces across two attempts), it also doesn't repro reliably outside a live collaborative session, which made isolating it harder — a synthetic (non-live-DOM) BlockNoteEditor.create() reproduction with the exact same schema/config and exact same operation did not reproduce the crash, suggesting it depends on live collaborative/DOM state.

Contribution

  • I'd be interested in contributing a fix for this issue

Sponsor

  • I'm a sponsor and would appreciate if you could look into this sooner than later 💖

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssue has not yet been reviewed or classified by maintainers.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions