{"record":{"id":"6844f3fe497b6bbd","repo":"ruvnet/ruflo","slug":"cannot-add-a-sibling-to-a-legacy-conversation","errorCode":null,"errorMessage":"Cannot add a sibling to a legacy conversation","messagePattern":"Cannot add a sibling to a legacy conversation","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/utils/tree/addSibling.ts","lineNumber":9,"sourceCode":"import { v4 } from \"uuid\";\nimport type { Tree, TreeId, NewNode, TreeNode } from \"./tree\";\n\nexport function addSibling<T>(conv: Tree<T>, message: NewNode<T>, siblingId: TreeId): TreeId {\n\tif (conv.messages.length === 0) {\n\t\tthrow new Error(\"Cannot add a sibling to an empty conversation\");\n\t}\n\tif (!conv.rootMessageId) {\n\t\tthrow new Error(\"Cannot add a sibling to a legacy conversation\");\n\t}\n\n\tconst sibling = conv.messages.find((m) => m.id === siblingId);\n\n\tif (!sibling) {\n\t\tthrow new Error(\"The sibling message doesn't exist\");\n\t}\n\n\tif (!sibling.ancestors || sibling.ancestors?.length === 0) {\n\t\tthrow new Error(\"The sibling message is the root message, therefore we can't add a sibling\");\n\t}\n\n\tconst messageId = v4();\n\n\tconv.messages.push({\n\t\t...message,\n\t\tid: messageId,\n\t\tancestors: sibling.ancestors,","sourceCodeStart":1,"sourceCodeEnd":27,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/utils/tree/addSibling.ts#L1-L27","documentation":"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).","triggerScenarios":"Calling addSibling (e.g. a \"regenerate this response\" action) on a conversation loaded from old data that was never migrated to the tree schema.","commonSituations":"Pre-migration records in the DB; importing conversations from another system as a flat message list; an older deployment that wrote messages without rootMessageId.","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."],"exampleFix":"// before\naddSibling(legacyConv, message, siblingId); // throws\n// after\nimport { convertLegacyConversation } from \"$lib/utils/tree/convertLegacyConversation\";\nconst treeConv = !legacyConv.rootMessageId ? convertLegacyConversation(legacyConv) : legacyConv;\naddSibling(treeConv, message, siblingId);","handlingStrategy":"validation","validationCode":"import { convertLegacyConversation } from \"$lib/utils/tree/convertLegacyConversation\";\nif (!conv.rootMessageId && conv.messages.length > 0) {\n  conv = convertLegacyConversation(conv);\n}\naddSibling(conv, message, siblingId);","typeGuard":"function isLegacyConversation<T>(conv: Tree<T>): boolean {\n  return conv.messages.length > 0 && !conv.rootMessageId;\n}","tryCatchPattern":"try {\n  addSibling(conv, message, siblingId);\n} catch (e) {\n  if (String((e as Error)?.message).includes(\"legacy conversation\")) {\n    addSibling(convertLegacyConversation(conv), message, siblingId);\n  } else throw e;\n}","preventionTips":["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."],"tags":["tree","conversation","legacy","migration"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}