{"record":{"id":"f131402962aa1522","repo":"ruvnet/ruflo","slug":"this-is-a-legacy-conversation-you-can-only-append","errorCode":null,"errorMessage":"This is a legacy conversation, you can only append to the last message","messagePattern":"This is a legacy conversation, you can only append to the last message","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/utils/tree/addChildren.ts","lineNumber":25,"sourceCode":"\t\tconst messageId = v4();\n\t\tconv.rootMessageId = messageId;\n\t\tconv.messages.push({\n\t\t\t...message,\n\t\t\tancestors: [],\n\t\t\tid: messageId,\n\t\t} as TreeNode<T>);\n\t\treturn messageId;\n\t}\n\n\tif (!parentId) {\n\t\tthrow new Error(\"You need to specify a parentId if this is not the first message\");\n\t}\n\n\tconst messageId = v4();\n\tif (!conv.rootMessageId) {\n\t\t// if there is no parentId we just push the message\n\t\tif (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {\n\t\t\tthrow new Error(\"This is a legacy conversation, you can only append to the last message\");\n\t\t}\n\t\tconv.messages.push({ ...message, id: messageId } as TreeNode<T>);\n\t\treturn messageId;\n\t}\n\n\tconst ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId];\n\tconv.messages.push({\n\t\t...message,\n\t\tancestors,\n\t\tid: messageId,\n\t\tchildren: [],\n\t} as TreeNode<T>);\n\n\tconst parent = conv.messages.find((m) => m.id === parentId);\n\n\tif (parent) {\n\t\tif (parent.children) {\n\t\t\tparent.children.push(messageId);","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/utils/tree/addChildren.ts#L7-L43","documentation":"Thrown by addChildren for a legacy conversation: one that has messages but no rootMessageId. Legacy conversations are a flat append-only list, so the function only allows appending to the current tail; passing a parentId that is not the last message's id is rejected because branching is unsupported without a proper tree.","triggerScenarios":"Loading an old conversation that predates the tree schema (rootMessageId missing) and attempting to branch off a non-tail message, or to reply to an earlier message in the history.","commonSituations":"Migrating an old dataset; a record was written by an older build that did not set rootMessageId; the user clicked \"edit and resend\" on a historical message in a legacy conversation.","solutions":["Run the legacy conversation through convertLegacyConversation (see the tree/ utilities) to populate rootMessageId and ancestor chains before branching.","Alternatively, only append to conv.messages[conv.messages.length - 1].id until migration is done.","Backfill rootMessageId for existing records in MongoDB so future loads are tree-shaped."],"exampleFix":"// before\naddChildren(legacyConv, message, someOldMessageId); // throws\n// after\nimport { convertLegacyConversation } from \"$lib/utils/tree/convertLegacyConversation\";\nconst treeConv = convertLegacyConversation(legacyConv);\naddChildren(treeConv, message, someOldMessageId);","handlingStrategy":"validation","validationCode":"import { convertLegacyConversation } from \"$lib/utils/tree/convertLegacyConversation\";\nfunction ensureTree<T>(conv: Tree<T>): Tree<T> {\n  if (conv.messages.length > 0 && !conv.rootMessageId) {\n    return convertLegacyConversation(conv);\n  }\n  return conv;\n}\naddChildren(ensureTree(conv), message, parentId);","typeGuard":"function isLegacyConversation<T>(conv: Tree<T>): boolean {\n  return conv.messages.length > 0 && !conv.rootMessageId;\n}","tryCatchPattern":"try {\n  addChildren(conv, message, parentId);\n} catch (e) {\n  if (String((e as Error)?.message).includes(\"legacy conversation\")) {\n    addChildren(convertLegacyConversation(conv), message, parentId);\n  } else throw e;\n}","preventionTips":["Backfill rootMessageId + ancestors for all legacy records in MongoDB.","Run every loaded conversation through convertLegacyConversation at the data-access boundary.","Hide branch/edit affordances until the conversation is confirmed non-legacy."],"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"}