ruvnet/ruflo · error · Error

This is a legacy conversation, you can only append to the la

Error message

This is a legacy conversation, you can only append to the last message

What it means

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.

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 6b01dc5a68)

Solutions

  1. Run the legacy conversation through convertLegacyConversation (see the tree/ utilities) to populate rootMessageId and ancestor chains before branching.
  2. Alternatively, only append to conv.messages[conv.messages.length - 1].id until migration is done.
  3. Backfill rootMessageId for existing records in MongoDB so future loads are tree-shaped.

Example fix

// before
addChildren(legacyConv, message, someOldMessageId); // throws
// after
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
const treeConv = convertLegacyConversation(legacyConv);
addChildren(treeConv, message, someOldMessageId);
Defensive patterns

Strategy: validation

Validate before calling

import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
function ensureTree<T>(conv: Tree<T>): Tree<T> {
  if (conv.messages.length > 0 && !conv.rootMessageId) {
    return convertLegacyConversation(conv);
  }
  return conv;
}
addChildren(ensureTree(conv), message, parentId);

Type guard

function isLegacyConversation<T>(conv: Tree<T>): boolean {
  return conv.messages.length > 0 && !conv.rootMessageId;
}

Try / catch

try {
  addChildren(conv, message, parentId);
} catch (e) {
  if (String((e as Error)?.message).includes("legacy conversation")) {
    addChildren(convertLegacyConversation(conv), message, parentId);
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/f131402962aa1522. Report an issue: GitHub.