Skip to content

v2 comments: replies refused (INVALID_CONTEXT) and resolve/reopen do not change status (docx-engine 0.26.0) #4124

Closed as not planned

Description

@simon-lighthouse

Summary

On docx-engine 0.26.0 (superdoc 2.27.0 in the browser, @superdoc/sdk 2.13.0 headless), two parts of the documented comment lifecycle in https://docs.superdoc.dev/document-api/comments do not persist:

  1. Replies. doc.comments.create({ parentCommentId, text }) is refused headless with INVALID_CONTEXT / unsupported-context. In the browser the same call (and ui.comments.reply()) returns success: true, but the reply is stored as a parentless root with no anchor: list() shows it with no parentCommentId / rootCommentId and no target, before and after reload.
  2. Resolve / reopen. doc.comments.patch({ commentId, status: 'resolved' }) returns success: true, but list({ includeResolved: true }) still reports status: 'open' and a following patch({ status: 'active' }) is refused with INVALID_INPUT. The saved DOCX shows why: commentsExtended.xml gains a new <w15:commentEx w15:done="1"> row under a paraId that is not the comment's own w14:paraId, while the comment's existing row keeps w15:done="0".

A side effect of (2): after any comment write, imported replies lose their threading in list() (an imported comment that listed with parentCommentId: "0" before the write lists with none after it), until the document is reopened.

Same result with ui.comments: true and false, in a v2 collaboration room and in a plain local open, so this is the engine rather than the collaboration path or the built-in UI config.

Reproduce (headless, no room)

Fixture: any DOCX with a comments.xml carrying three comments and a commentsExtended.xml that marks the second as a reply (w15:paraIdParent). Ours is a two-page NDA with those parts injected; happy to attach it.

import { SuperDocClient } from '@superdoc/sdk';

const client = new SuperDocClient({ runtime: 'v2' });
await client.connect();
const doc = await client.open({ doc: './nda-with-comments.docx', runtime: 'v2' });

const list0 = await doc.comments.list({ includeResolved: true });
// list0.items[1] -> { commentId: '1', parentCommentId: '0', status: 'open' }

const found = await doc.find({ select: { type: 'text', pattern: 'Gardening Services Limited' } });
const blockId = found.items[0].address.nodeId;
const created = await doc.comments.create({
  target: { kind: 'text', segments: [{ blockId, range: { start: 0, end: 26 } }] },
  text: 'Repro root',
  expectedRevision: list0.evaluatedRevision,
});
// created -> { success: true, id: '3' }

const list1 = await doc.comments.list({ includeResolved: true });
await doc.comments.create({ parentId: created.id, text: 'Repro reply', expectedRevision: list1.evaluatedRevision });
// throws SuperDocCliError INVALID_CONTEXT "unsupported-context" (also without expectedRevision,
// also with parentCommentId, also replying to the imported root '0')

const list2 = await doc.comments.list({ includeResolved: true });
await doc.comments.patch({ id: created.id, status: 'resolved', expectedRevision: list2.evaluatedRevision });
// -> { success: true }
const list3 = await doc.comments.list({ includeResolved: true });
// list3.items.find(c => c.commentId === created.id).status -> 'open'
// list3.items[1].parentCommentId -> undefined   (imported reply has lost its parent)

await doc.comments.patch({ id: created.id, status: 'active', expectedRevision: list3.evaluatedRevision });
// throws SuperDocCliError INVALID_INPUT "invalid-input"

await doc.save({ out: './out.docx', force: true });

out.docx → word/commentsExtended.xml:

<w15:commentEx w15:paraId="01000000" w15:done="0"/>
<w15:commentEx w15:paraId="01000001" w15:paraIdParent="01000000" w15:done="0"/>
<w15:commentEx w15:paraId="01000002" w15:done="0"/>
<w15:commentEx w15:paraId="10EDD16B" w15:done="0"/>
<w15:commentEx w15:paraId="0FEE782F" w15:done="1"/>   <!-- not the paraId of any comment paragraph -->

Browser (superdoc 2.27.0, v2 hocuspocus room)

ui.comments.reply(rootId, { text }) → { success: true, id: '4_e186' }; ui.comments.list() then contains { id: '4_e186', text: 'Spike reply', status: 'open' } with no parentCommentId, rootCommentId or target, in this tab, in a second tab, and after reload. ui.comments.resolve(id) → { success: true, updated: [...] } while doc.comments.get({ commentId: id }).status stays 'open'; reopen(id) → INVALID_INPUT.

Expected

Per the Document API guide: the reply lists under its parent with the root's anchor, and patch({ status }) flips status on read and w15:done on the comment's own commentEx row.

Versions

  • superdoc 2.27.0, @superdoc/docx-engine 0.26.0, @superdoc/sdk 2.13.0 (hoisted engine 0.26.0)
  • Node 24, macOS 27; Chrome for the browser notes
  • Not yet retested on 2.28.0

Written by Claude Fable 5.1 on Simon Vollmer's behalf.

Activity

  1. simon-lighthouse commented on Oct 31, 2026

    @simon-lighthouse
    Author

    Closing: this was my test fixture, not the engine. Apologies for the noise.

    The DOCX I injected comments into declared xmlns:w14 on each <w:p> inside comments.xml rather than on the <w:comments> root. The engine resolves w14:paraId by namespace, so every comment in that part parsed with no paraId, which produced both symptoms: replies could not thread onto the parent (unsupported-context), and each patch({ status: 'resolved' }) minted a fresh paraId for its commentsExtended.xml row (the done="1" rows under unrelated ids), so the status never read back.

    With xmlns:w14 on the root, and separately on a document with no prior comments, the documented sequence (create with a query target, create({ parentCommentId }), patch({ commentId, status: 'resolved' }), patch({ status: 'active' }), each with a fresh expectedRevision) works end to end on docx-engine 0.26.0: the reply lists under its parent, resolve flips root and reply to resolved and includeResolved: false omits them, reopen returns them to open.

    One small observation that might still be worth a look: when the engine appends a comment to a comments.xml whose root lacks the w14 binding, it writes <w:p w14:paraId="…"> without adding the declaration, leaving the part with an unbound prefix. A guard that declares xmlns:w14 on the root in that case would make malformed inputs self-heal. Happy to open that as a separate, smaller issue if useful.

    Written by Claude Fable 5.1 on Simon Vollmer's behalf.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions