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:
client.open({ doc: <blank.docx>, collaboration: { providerType: "hocuspocus", url, documentId } }) (fresh room, or roomMode: "create").
toolkit.dispatch(doc, "superdoc_edit", { action: "insert", type: "markdown", value: TABLE_MD }, { changeMode: "tracked" }) → success receipt.
await doc.trackChanges.list() → PRECONDITION_FAILED with the "could not resolve the current table span" message.
Variant B:
- Same open.
- Insert a plain paragraph via
superdoc_edit (type: "markdown", e.g. "Intro paragraph") → ok, reads ok.
- Insert
TABLE_MD with placement: "after" anchored to that paragraph → ok, reads ok, table visible.
- Insert another paragraph with
placement: "after" anchored to the paragraph following the table → success receipt.
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.
What happened?
Follow-up to a report we sent through support: in a live v2 collaboration room (Hocuspocus), a
superdoc_editMarkdown 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 hittable 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:
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:
The
tbl:id in variant B is the table created by the preceding Markdown insert.Answers to the questions from the support thread:
doc.trackChanges.list()followed bydoc.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-runsrefreshCleanRecord()synchronously and returns the failure.createAgentToolkit({ provider: "vercel", preset: "legacy" }), thentoolkit.dispatch(doc, "superdoc_edit", args, { changeMode: "tracked" }). Variant A also reproduces withchangeMode: "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, thenassertCurrentTableSpanMatchesBaseXmlslices 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.resolveCurrentTableStructuralSpanandassertCurrentTableSpanMatchesBaseXmlare byte-identical in the 2.9.0sdk-darwin-arm64binary, 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_createtable + per-cellset_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.collaborationwithproviderType: "hocuspocus"). We have not yet isolated whether a browser client being connected to the room matters.Variant A:
client.open({ doc: <blank.docx>, collaboration: { providerType: "hocuspocus", url, documentId } })(fresh room, orroomMode: "create").toolkit.dispatch(doc, "superdoc_edit", { action: "insert", type: "markdown", value: TABLE_MD }, { changeMode: "tracked" })→ success receipt.await doc.trackChanges.list()→PRECONDITION_FAILEDwith the "could not resolve the current table span" message.Variant B:
superdoc_edit(type: "markdown", e.g."Intro paragraph") → ok, reads ok.TABLE_MDwithplacement: "after"anchored to that paragraph → ok, reads ok, table visible.placement: "after"anchored to the paragraph following the table → success receipt.await doc.trackChanges.list()→PRECONDITION_FAILEDwith the "overlay coordinates do not resolve to a single current <w:tbl> span" message, naming the table from step 3.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 withCAPABILITY_UNSUPPORTED: Rich insertion relative to a table target is not supported; use a paragraph anchor.That's fine as a limitation, but thesuperdoc_edittool 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_DIRset and attach the trace for either variant if that helps.