ruvnet/ruflo · error · Error
Cannot add a sibling to a legacy conversation
Error message
Cannot add a sibling to a legacy conversation
What it means
Thrown by addSibling when the conversation has messages but conv.rootMessageId is falsy — the legacy shape. addSibling needs the tree's ancestor links to attach the new node as a branch off the sibling's parent, which a flat legacy list does not have, so the operation is refused outright (unlike addChildren, which tolerates append-to-tail on legacy data).
Source
Thrown at ruflo/src/ruvocal/src/lib/utils/tree/addSibling.ts:9
import { v4 } from "uuid";
import type { Tree, TreeId, NewNode, TreeNode } from "./tree";
export function addSibling<T>(conv: Tree<T>, message: NewNode<T>, siblingId: TreeId): TreeId {
if (conv.messages.length === 0) {
throw new Error("Cannot add a sibling to an empty conversation");
}
if (!conv.rootMessageId) {
throw new Error("Cannot add a sibling to a legacy conversation");
}
const sibling = conv.messages.find((m) => m.id === siblingId);
if (!sibling) {
throw new Error("The sibling message doesn't exist");
}
if (!sibling.ancestors || sibling.ancestors?.length === 0) {
throw new Error("The sibling message is the root message, therefore we can't add a sibling");
}
const messageId = v4();
conv.messages.push({
...message,
id: messageId,
ancestors: sibling.ancestors,View on GitHub (pinned to 6b01dc5a68)
Solutions
- Convert the conversation with convertLegacyConversation before exposing sibling/branch actions.
- Backfill rootMessageId + ancestors for legacy records in bulk.
- Hide the "regenerate / branch" UI affordance for conversations where !conv.rootMessageId until migration completes.
Example fix
// before
addSibling(legacyConv, message, siblingId); // throws
// after
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
const treeConv = !legacyConv.rootMessageId ? convertLegacyConversation(legacyConv) : legacyConv;
addSibling(treeConv, message, siblingId); Defensive patterns
Strategy: validation
Validate before calling
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
if (!conv.rootMessageId && conv.messages.length > 0) {
conv = convertLegacyConversation(conv);
}
addSibling(conv, message, siblingId); Type guard
function isLegacyConversation<T>(conv: Tree<T>): boolean {
return conv.messages.length > 0 && !conv.rootMessageId;
} Try / catch
try {
addSibling(conv, message, siblingId);
} catch (e) {
if (String((e as Error)?.message).includes("legacy conversation")) {
addSibling(convertLegacyConversation(conv), message, siblingId);
} else throw e;
} Prevention
- Migrate legacy conversations before exposing sibling/branch UI.
- Bulk-backfill rootMessageId so the legacy path is never hit in production.
- Assert conv.rootMessageId is set at the repository load boundary.
When it happens
Trigger: Calling addSibling (e.g. a "regenerate this response" action) on a conversation loaded from old data that was never migrated to the tree schema.
Common situations: Pre-migration records in the DB; importing conversations from another system as a flat message list; an older deployment that wrote messages without rootMessageId.
Related errors
- This is a legacy conversation, you can only append to the la
- You need to specify a parentId if this is not the first mess
- Cannot add a sibling to an empty conversation
- The sibling message doesn't exist
- The sibling message is the root message, therefore we can't
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/6844f3fe497b6bbd.
Report an issue: GitHub.