jackwener/OpenCLI · error · CommandExecutionError

Zhihu answer comment ${id} had conflicting root provenance

Error message

Zhihu answer comment ${id} had conflicting root provenance

What it means

For a child (reply) comment, describeComment normalizes reply_root_comment_id and requires it to equal the expectedRootId of the root comment thread being fetched. A mismatch means the API returned a reply whose declared root does not match the thread it appeared under. The library throws to avoid attaching a reply to the wrong root in the reconstructed tree.

Source

Thrown at clis/zhihu/answer-comments-helpers.js:98

    }
    if (!Array.isArray(payload.data) || !payload.paging || typeof payload.paging !== 'object') {
        throw new CommandExecutionError(`Zhihu ${label} returned a malformed payload`);
    }
    if (typeof payload.paging.is_end !== 'boolean') {
        throw new CommandExecutionError(`Zhihu ${label} returned malformed paging state`);
    }
    return payload;
}
function describeComment(comment, role, expectedRootId = '') {
    if (!comment || typeof comment !== 'object' || Array.isArray(comment)) {
        throw new CommandExecutionError(`Zhihu answer comments contained a malformed ${role} row`);
    }
    const id = commentId(comment.id);
    if (!id) throw new CommandExecutionError(`Zhihu answer comments contained a ${role} row without a stable id`);
    const rootId = role === 'root' ? id : commentId(comment.reply_root_comment_id);
    const parentId = role === 'root' ? '' : commentId(comment.reply_comment_id);
    if (role === 'child' && rootId !== expectedRootId) {
        throw new CommandExecutionError(`Zhihu answer comment ${id} had conflicting root provenance`);
    }
    if (role === 'child' && !parentId) {
        throw new CommandExecutionError(`Zhihu answer comment ${id} did not identify its immediate parent`);
    }
    const signature = JSON.stringify({
        role,
        rootId,
        parentId,
        author: memberName(comment.author),
        replyTo: role === 'child' ? memberName(comment.reply_to_author) : '',
        content: stripHtml(comment.content || ''),
    });
    return { id, rootId, parentId, signature };
}
function addPageComments(byId, comments, role, expectedRootId) {
    for (const comment of comments) {
        const descriptor = describeComment(comment, role, expectedRootId);
        const previous = byId.get(descriptor.id);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log the child's reply_root_comment_id and the expectedRootId to see whether the API re-parented the reply or the expected id is wrong.
  2. Refetch the child-comment page; if Zhihu moved the reply, fresh data should be consistent.
  3. If intentionally fetching replies of multiple roots in one pass, call fetchPages per root with the correct expectedRootId instead of merging pages.
  4. Upgrade the library if Zhihu changed reply provenance fields (e.g. reply_root_comment_id renamed).

Example fix

// before: fetching children of root '111' but page contains reply rooted at '222'
fetchChildComments(page, '111', limit); // throws for the '222' row
// after: fetch per root, or pre-filter
const filtered = rows.filter(r => commentId(r.reply_root_comment_id) === '111');
Defensive patterns

Strategy: validation

Validate before calling

// ensure every child row's declared root matches the thread you are fetching
function filterChildrenForRoot(rows, expectedRootId) {
  return rows.filter(r => String(r.reply_root_comment_id ?? '') === String(expectedRootId));
}

Type guard

function belongsToRoot(child, rootId) {
  return typeof child?.reply_root_comment_id === 'string' &&
    child.reply_root_comment_id === String(rootId);
}

Try / catch

try {
  const replies = await fetchChildComments(page, rootId, limit);
} catch (err) {
  if (String(err.message).includes('conflicting root provenance')) {
    console.warn(`Root ${rootId} returned replies parented elsewhere; refetching`);
    return fetchChildComments(page, rootId, limit); // single retry for re-parented data
  }
  throw err;
}

Prevention

When it happens

Trigger: fetchChildComments requests replies for root X but the payload contains rows whose reply_root_comment_id normalizes to a different id than X. This happens when describeComment(child, 'child', expectedRootId) sees reply_root_comment_id null/absent/different.

Common situations: Zhihu re-parenting a reply (moved to another thread), caching layers serving stale mixed pages, or a bug in the caller passing the wrong expectedRootId when manually invoking fetchPages/fetchChildComments.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/7196888d16b0090d. Report an issue: GitHub.