{"record":{"id":"897f65095e8db77d","repo":"amruthpillai/reactive-resume","slug":"bad-request","errorCode":"BAD_REQUEST","errorMessage":"No matching unanswered user question was found.","messagePattern":"No matching unanswered user question was found\\.","errorType":"http","errorClass":"ORPCError","httpStatus":400,"severity":"error","filePath":"packages/api/src/features/agent/service.ts","lineNumber":247,"sourceCode":"\n\t\tdidMerge = true;\n\t\tif (answeredPart.state === \"output-error\") {\n\t\t\treturn {\n\t\t\t\t...part,\n\t\t\t\tstate: \"output-error\",\n\t\t\t\terrorText: answeredPart.errorText ?? \"User answer failed.\",\n\t\t\t} as UIMessage[\"parts\"][number];\n\t\t}\n\n\t\treturn {\n\t\t\t...part,\n\t\t\tstate: \"output-available\",\n\t\t\toutput: answeredPart.output,\n\t\t} as UIMessage[\"parts\"][number];\n\t});\n\n\tif (!didMerge) {\n\t\tthrow new ORPCError(\"BAD_REQUEST\", { message: \"No matching unanswered user question was found.\" });\n\t}\n\n\treturn { ...existingMessage, parts };\n}\n\nfunction getFirstUnansweredAskUserQuestionToolCallId(message: UIMessage) {\n\tconst part = message.parts.find((part) => {\n\t\tconst toolPart = part as AgentToolPart;\n\t\treturn (\n\t\t\ttoolPart.type === \"tool-ask_user_question\" &&\n\t\t\ttypeof toolPart.toolCallId === \"string\" &&\n\t\t\ttoolPart.state === \"input-available\"\n\t\t);\n\t}) as AgentToolPart | undefined;\n\n\treturn part?.toolCallId;\n}\n","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/amruthpillai/reactive-resume/blob/3a5b12e2a40374a9571988701fcb75c5a1831c42/packages/api/src/features/agent/service.ts#L229-L265","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["On the client, only submit answers using toolCallIds harvested from the latest assistant message parts that are still in state 'input-available'.","Make the submit-answer call idempotent: ignore a 400 'No matching unanswered user question' if the local UI already shows the question as answered.","Refresh the thread's messages before re-submitting so the client sees the current part states.","Confirm the message.id being sent matches the assistant message id returned by the server."],"exampleFix":"// before: client invents / reuses a toolCallId\nconst toolCallId = lastSubmittedId ?? makeId();\nawait orpc.agent.messages.send({ message: { role: 'assistant', parts: [{ type: 'tool-ask_user_question', toolCallId, state: 'output-available', output: answer }] } });\n\n// after: read the unanswered part from the live assistant message\nconst unanswered = assistantMsg.parts.find(p => p.type === 'tool-ask_user_question' && p.state === 'input-available');\nif (!unanswered) return; // nothing to answer\nawait orpc.agent.messages.send({ message: { role: 'assistant', parts: [{ type: 'tool-ask_user_question', toolCallId: unanswered.toolCallId, state: 'output-available', output: answer }] } });","handlingStrategy":"validation","validationCode":"const unanswered = assistantMsg.parts.find(\n  (p): p is Extract<typeof p, { type: 'tool-ask_user_question' }> =>\n    p.type === 'tool-ask_user_question' && (p as any).state === 'input-available' && typeof (p as any).toolCallId === 'string',\n);\nif (!unanswered) return; // nothing to submit","typeGuard":"function isUnansweredAskUserQuestionPart(part: any): boolean {\n  return part?.type === 'tool-ask_user_question' && part?.state === 'input-available' && typeof part?.toolCallId === 'string';\n}","tryCatchPattern":"try {\n  await orpc.agent.messages.send({ threadId, message });\n} catch (err) {\n  if (err instanceof ORPCError && err.code === 'BAD_REQUEST' && /no matching unanswered/.test(err.message.toLowerCase())) {\n    // Already answered or stale; refresh messages and move on.\n    await refreshThread();\n    return;\n  }\n  throw err;\n}","preventionTips":["Drive submit only from the live assistant message parts, never from cached IDs.","Single-flight the submit-answer button so retries cannot race.","After a successful submit, mark the part as answered locally to prevent re-submit."],"tags":["agent","tool-call","ask-user-question","validation","idempotency","orpc"],"backgroundTag":null,"analyzedSha":"3a5b12e2a40374a9571988701fcb75c5a1831c42","analyzedAt":"2026-08-12T22:31:22.666Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}