ruvnet/ruflo · error · Error

You need to specify a parentId if this is not the first mess

Error message

You need to specify a parentId if this is not the first message

What it means

Thrown by addChildren when the conversation already has at least one message (messages.length !== 0) but no parentId argument was supplied. The function uses parentId to compute the new node's ancestors chain and to register the child under the parent, so omitting it on a non-empty tree is ambiguous and rejected.

Source

Thrown at ruflo/src/ruvocal/src/lib/utils/tree/addChildren.ts:18

import { v4 } from "uuid";
import type { Tree, TreeId, NewNode, TreeNode } from "./tree";

export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: TreeId): TreeId {
	// if this is the first message we just push it
	if (conv.messages.length === 0) {
		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: [],

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Pass the id of the message this new message replies to as parentId.
  2. If you genuinely want a new root, create a fresh conversation object instead of reusing the existing one.
  3. At the call site, assert conv.messages.length === 0 before going parentless, or always pass parentId otherwise.

Example fix

// before
addChildren(conv, message);
// after
const parentId = conv.messages.length === 0 ? undefined : conv.messages[conv.messages.length - 1].id;
addChildren(conv, message, parentId);
Defensive patterns

Strategy: validation

Validate before calling

function appendMessage<T>(conv: Tree<T>, message: NewNode<T>, parentId?: string) {
  if (conv.messages.length > 0 && !parentId) {
    throw new Error("parentId required: conversation is not empty");
  }
  return addChildren(conv, message, parentId);
}

Type guard

function isEmptyTree<T>(conv: Tree<T>): boolean {
  return conv.messages.length === 0;
}

Try / catch

try {
  addChildren(conv, message, parentId);
} catch (e) {
  if (String((e as Error)?.message).includes("parentId")) {
    parentId = conv.messages[conv.messages.length - 1]?.id;
    addChildren(conv, message, parentId);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling addChildren(conv, message) without a third argument on a conversation that is not empty; a UI flow that creates the second message but lost track of which message it replies to.

Common situations: Refactoring a caller that previously always passed parentId and dropping the argument; replaying a message log where only the first entry had no parent; a race where two addChildren calls raced and the second no longer sees an empty array.

Related errors


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