Skip to content

v2 collaboration (Node SDK): Markdown insert containing a table -> table structural projection span drift on the next read (on creation, and stale overlay after a following insert) #3985

Description

@slaznik

What happened?

Follow-up to a report we sent through support: in a live v2 collaboration room (Hocuspocus), a superdoc_edit Markdown insert that contains a table breaks the SDK's collaboration projection. On @superdoc/sdk@2.4.0 + superdoc@2.7.0, Markdown/HTML inserts land as proper tracked changes in both file and collab sessions (thanks!), but table inserts in collab still hit table structural projection span drift, in two forms.

Variant A — on creation (original report). Fresh blank doc, single Markdown insert containing a table, direct or tracked mode:

collaboration projection refresh failed: table structural projection span drift
in main:/word/document.xml for tbl:4524F180: could not resolve the current table
span from the projected story

Variant B — after a neighbouring edit (new). When the table insert does succeed (some text, then one Markdown table), the very next insert placed after the table fails:

collaboration projection refresh failed: table structural projection span drift
in main:/word/document.xml for tbl:664082E2: overlay coordinates do not resolve
to a single current <w:tbl> span

The tbl: id in variant B is the table created by the preceding Markdown insert.

Answers to the questions from the support thread:

  • When does the error surface? Not on the insert itself. The insert dispatch returns a success receipt and the table is visible in the room. The error comes back from the next Document API read on the same session — in our case doc.trackChanges.list() followed by doc.getText(), which we call immediately after every dispatch. That matches what we see in the bundle: after a mutation the refresh is scheduled on a zero-delay timer with its error swallowed, and the next read goes through the read-freshness gate, which re-runs refreshCleanRecord() synchronously and returns the failure.
  • Tool call or direct Document API? Tool call. createAgentToolkit({ provider: "vercel", preset: "legacy" }), then toolkit.dispatch(doc, "superdoc_edit", args, { changeMode: "tracked" }). Variant A also reproduces with changeMode: "direct".

Reading the minified bundle, our understanding of variant B: the successful table insert leaves a structural overlay recording the table's byte span in the projected document.xml. On the next non-table patch, remapBlockDigestsAfterTableOverlays-adjacent code rebases each overlay by the length delta of patches that start before it, then assertCurrentTableSpanMatchesBaseXml slices the XML at the rebased coordinates and requires exactly one <w:tbl>…</w:tbl> (looksLikeSingleTableXml). A paragraph inserted directly after the table makes that slice stale, so the assert throws. resolveCurrentTableStructuralSpan and assertCurrentTableSpanMatchesBaseXml are byte-identical in the 2.9.0 sdk-darwin-arm64 binary, so we did not expect a bump alone to change this.

The bytes-opened (non-collab) path is unaffected for both variants. Workaround is superdoc_create table + per-cell set_cell_text, i.e. one tool call per cell instead of one Markdown insert, which we'd rather avoid. Happy to test a fix build.

Steps to reproduce

Hocuspocus v2 room, SDK joining the room server-side (DocOpenParams.collaboration with providerType: "hocuspocus"). We have not yet isolated whether a browser client being connected to the room matters.

Variant A:

  1. client.open({ doc: <blank.docx>, collaboration: { providerType: "hocuspocus", url, documentId } }) (fresh room, or roomMode: "create").
  2. toolkit.dispatch(doc, "superdoc_edit", { action: "insert", type: "markdown", value: TABLE_MD }, { changeMode: "tracked" }) → success receipt.
  3. await doc.trackChanges.list() → PRECONDITION_FAILED with the "could not resolve the current table span" message.

Variant B:

  1. Same open.
  2. Insert a plain paragraph via superdoc_edit (type: "markdown", e.g. "Intro paragraph") → ok, reads ok.
  3. Insert TABLE_MD with placement: "after" anchored to that paragraph → ok, reads ok, table visible.
  4. Insert another paragraph with placement: "after" anchored to the paragraph following the table → success receipt.
  5. await doc.trackChanges.list() → PRECONDITION_FAILED with the "overlay coordinates do not resolve to a single current <w:tbl> span" message, naming the table from step 3.
TABLE_MD =
| Responsibility | Assigned to | Priority | Deadline |
|---|---|---|---|
| Review the document | Legal team | High | To be confirmed |
| Collect supporting information | Project team | Medium | To be confirmed |
| Identify open issues | Responsible stakeholders | High | To be confirmed |
| Approve next steps | Decision-maker | High | To be confirmed |

Side note from the same flow: anchoring the Markdown insert to the table block itself (target: { kind: "block", nodeType: "table", nodeId }, placement: "after") is rejected with CAPABILITY_UNSUPPORTED: Rich insertion relative to a table target is not supported; use a paragraph anchor. That's fine as a limitation, but the superdoc_edit tool description and system prompt don't mention it, so an LLM only learns it by failing once. A sentence in the description would save a round trip.

SuperDoc version

@superdoc/sdk@2.4.0 (also checked the 2.9.0 binary for the guard code), superdoc@2.7.0, @superdoc/docx-engine@0.6.0, Hocuspocus v2 room.

Browser

Not applicable (server-side SDK).

Additional context

We can run with SUPERDOC_SDK_DEBUG_TRACE / SUPERDOC_SDK_DEBUG_TRACE_DIR set and attach the trace for either variant if that helps.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: doneAll linked engineering blockers are marked Done.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions