ruvnet/ruflo · error
This is a legacy conversation, you can only append to the…
Error message
This is a legacy conversation, you can only append to the last message
What it means
Thrown by addChildren() for legacy conversations — trees with messages but no rootMessageId, predating the ancestor-tracking format. Legacy trees carry no parent links, so insertion is only possible at the tail; passing a parentId that is not the id of the last message would orphan the node, hence the guard.
Solutions
- Append only to the last message of a legacy conversation: pass conv.messages[conv.messages.length - 1].id (or no parentId when it is empty).
- To branch anywhere, migrate the conversation to the tree format first (set rootMessageId and rebuild ancestors for each node).
- If legacy conversations are irrelevant in your deployment, archive or drop them.
- Write a one-time migration that walks messages in order and assigns rootMessageId + ancestors so addChildren gains full branching.
Example fix
// before (legacy conv, branching mid-history) addChildren(conv, message, someOldMessageId); // throws // after const lastId = conv.messages[conv.messages.length - 1].id; addChildren(conv, message, lastId);
Defensive patterns
Strategy: type-guard
Validate before calling
const isLegacy = !conv.rootMessageId;
if (isLegacy && parentId !== conv.messages[conv.messages.length - 1].id) {
throw new Error("legacy conversation supports append-only");
} Type guard
function isLegacyConversation<T>(conv: { rootMessageId?: string }): conv is { rootMessageId?: undefined } {
return conv.rootMessageId === undefined;
} Prevention
- Run a data migration that backfills rootMessageId and ancestors for old conversations.
- Disable mid-history branching UI when isLegacyConversation is true.
- Never assume persisted conversations are in the current format — check rootMessageId first.
When it happens
Trigger: conv.rootMessageId is undefined (old persisted conversation) and addChildren is called with a parentId different from conv.messages[conv.messages.length - 1].id — i.e., trying to branch off an arbitrary historical message.
Common situations: Reopening old conversations created before the tree migration and attempting to edit/branch a middle message; importing legacy data and replaying edit operations against it.
Related errors
- Cannot add a sibling to a legacy conversation
- Cannot add a sibling to an empty conversation
- Message not found
- The sibling message doesn't exist
- The sibling message is the root message, therefore we can't…
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/f131402962aa1522.
Report an issue: GitHub.
Appendix: source
Thrown at ruflo/src/ruvocal/src/lib/utils/tree/addChildren.ts:25
const messageId = v4();
conv.rootMessageId = messageId;
conv.messages.push({
...message,
ancestors: [],
id: messageId,
} as TreeNode<T>);
return messageId;
}
if (!parentId) {
throw new Error("You need to specify a parentId if this is not the first message");
}
const messageId = v4();
if (!conv.rootMessageId) {
// if there is no parentId we just push the message
if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
throw new Error("This is a legacy conversation, you can only append to the last message");
}
conv.messages.push({ ...message, id: messageId } as TreeNode<T>);
return messageId;
}
const ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId];
conv.messages.push({
...message,
ancestors,
id: messageId,
children: [],
} as TreeNode<T>);
const parent = conv.messages.find((m) => m.id === parentId);
if (parent) {
if (parent.children) {
parent.children.push(messageId);View on GitHub (pinned to fa13ee4ad6)