Repository navigation
v2 comments: replies refused (INVALID_CONTEXT) and resolve/reopen do not change status (docx-engine 0.26.0) #4124
Description
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:
- Replies.
doc.comments.create({ parentCommentId, text })is refused headless withINVALID_CONTEXT/unsupported-context. In the browser the same call (andui.comments.reply()) returnssuccess: true, but the reply is stored as a parentless root with no anchor:list()shows it with noparentCommentId/rootCommentIdand notarget, before and after reload. - Resolve / reopen.
doc.comments.patch({ commentId, status: 'resolved' })returnssuccess: true, butlist({ includeResolved: true })still reportsstatus: 'open'and a followingpatch({ status: 'active' })is refused withINVALID_INPUT. The saved DOCX shows why:commentsExtended.xmlgains a new<w15:commentEx w15:done="1">row under a paraId that is not the comment's ownw14:paraId, while the comment's existing row keepsw15: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
simon-lighthouse commentedon Oct 31, 2026
Closing: this was my test fixture, not the engine. Apologies for the noise.
The DOCX I injected comments into declared
xmlns:w14on each<w:p>insidecomments.xmlrather than on the<w:comments>root. The engine resolvesw14:paraIdby 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 eachpatch({ status: 'resolved' })minted a fresh paraId for itscommentsExtended.xmlrow (thedone="1"rows under unrelated ids), so the status never read back.With
xmlns:w14on the root, and separately on a document with no prior comments, the documented sequence (createwith a query target,create({ parentCommentId }),patch({ commentId, status: 'resolved' }),patch({ status: 'active' }), each with a freshexpectedRevision) works end to end on docx-engine 0.26.0: the reply lists under its parent, resolve flips root and reply toresolvedandincludeResolved: falseomits them, reopen returns them toopen.One small observation that might still be worth a look: when the engine appends a comment to a
comments.xmlwhose root lacks thew14binding, it writes<w:p w14:paraId="…">without adding the declaration, leaving the part with an unbound prefix. A guard that declaresxmlns:w14on 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.