amruthpillai/reactive-resume · error · ORPCError

BAD_REQUEST

BAD_REQUEST

Error message

No matching unanswered user question was found.

What it means

Thrown by mergeAskUserQuestionOutputs when the incoming assistant message contains one or more 'answered' tool-ask_user_question parts (state output-available or output-error) but NONE of their toolCallIds match an existing unanswered (state input-available) tool-ask_user_question part in the persisted assistant message. The didMerge flag stays false, so the server refuses to write a no-op update.

Source

Thrown at packages/api/src/features/agent/service.ts:247

		didMerge = true;
		if (answeredPart.state === "output-error") {
			return {
				...part,
				state: "output-error",
				errorText: answeredPart.errorText ?? "User answer failed.",
			} as UIMessage["parts"][number];
		}

		return {
			...part,
			state: "output-available",
			output: answeredPart.output,
		} as UIMessage["parts"][number];
	});

	if (!didMerge) {
		throw new ORPCError("BAD_REQUEST", { message: "No matching unanswered user question was found." });
	}

	return { ...existingMessage, parts };
}

function getFirstUnansweredAskUserQuestionToolCallId(message: UIMessage) {
	const part = message.parts.find((part) => {
		const toolPart = part as AgentToolPart;
		return (
			toolPart.type === "tool-ask_user_question" &&
			typeof toolPart.toolCallId === "string" &&
			toolPart.state === "input-available"
		);
	}) as AgentToolPart | undefined;

	return part?.toolCallId;
}

View on GitHub (pinned to 3a5b12e2a4)

Solutions

  1. On the client, only submit answers using toolCallIds harvested from the latest assistant message parts that are still in state 'input-available'.
  2. Make the submit-answer call idempotent: ignore a 400 'No matching unanswered user question' if the local UI already shows the question as answered.
  3. Refresh the thread's messages before re-submitting so the client sees the current part states.
  4. Confirm the message.id being sent matches the assistant message id returned by the server.

Example fix

// before: client invents / reuses a toolCallId
const toolCallId = lastSubmittedId ?? makeId();
await orpc.agent.messages.send({ message: { role: 'assistant', parts: [{ type: 'tool-ask_user_question', toolCallId, state: 'output-available', output: answer }] } });

// after: read the unanswered part from the live assistant message
const unanswered = assistantMsg.parts.find(p => p.type === 'tool-ask_user_question' && p.state === 'input-available');
if (!unanswered) return; // nothing to answer
await orpc.agent.messages.send({ message: { role: 'assistant', parts: [{ type: 'tool-ask_user_question', toolCallId: unanswered.toolCallId, state: 'output-available', output: answer }] } });
Defensive patterns

Strategy: validation

Validate before calling

const unanswered = assistantMsg.parts.find(
  (p): p is Extract<typeof p, { type: 'tool-ask_user_question' }> =>
    p.type === 'tool-ask_user_question' && (p as any).state === 'input-available' && typeof (p as any).toolCallId === 'string',
);
if (!unanswered) return; // nothing to submit

Type guard

function isUnansweredAskUserQuestionPart(part: any): boolean {
  return part?.type === 'tool-ask_user_question' && part?.state === 'input-available' && typeof part?.toolCallId === 'string';
}

Try / catch

try {
  await orpc.agent.messages.send({ threadId, message });
} catch (err) {
  if (err instanceof ORPCError && err.code === 'BAD_REQUEST' && /no matching unanswered/.test(err.message.toLowerCase())) {
    // Already answered or stale; refresh messages and move on.
    await refreshThread();
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Client sends an assistant-role message whose ask_user_question toolCallId was already answered; client fabricated a toolCallId that was never issued by the assistant; the underlying assistant message was rewritten/re-streamed so the original input-available part is gone; answering a question from a different thread/message.

Common situations: Frontend retries a 'submit answer' call after the first succeeded (idempotency not handled); stale UI state after the thread was re-streamed; bug in client that synthesizes a toolCallId instead of reading it from the assistant part; double-submit from a flaky network.

Related errors


AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12). Data as JSON: /api/errors/897f65095e8db77d. Report an issue: GitHub.