paperclipai/paperclip · error · Error

question_response_interaction_not_answered

Error message

question_response_interaction_not_answered

What it means

Thrown by buildQuestionResponseDeliveryEnvelope when an ask_user_questions interaction is not in a deliverable answered state. The library requires an interaction whose status is 'answered', with a non-null result, and whose result.cancelled is not true, before it can build the durable delivery envelope. It is an internal invariant guard against attempting delivery of an unanswered or cancelled question.

Solutions

  1. Fetch the interaction fresh and verify interaction.status === 'answered' before building the envelope
  2. Check interaction.result is non-null and interaction.result.cancelled !== true; skip delivery for cancelled interactions
  3. Treat the error as a skip signal: mark the delivery as not-applicable instead of retrying
  4. If this fires unexpectedly in normal flow, check for a race between interaction resolution and delivery scheduling and re-read state

Example fix

// before
const envelope = buildQuestionResponseDeliveryEnvelope(interaction);
// after
if (interaction.status === "answered" && interaction.result && !interaction.result.cancelled) {
  const envelope = buildQuestionResponseDeliveryEnvelope(interaction);
} else {
  logger.info({ interactionId: interaction.id }, "question interaction not deliverable; skipping");
}
Defensive patterns

Strategy: validation

Validate before calling

function isDeliverable(i) {
  return i.status === "answered" && i.result != null && i.result.cancelled !== true;
}
if (!isDeliverable(interaction)) throw new SkipDeliveryError(interaction.id);

Type guard

function isAnsweredInteraction(i) {
  return i.status === "answered" &&
    typeof i.result === "object" && i.result !== null &&
    ("cancelled" in i.result ? i.result.cancelled !== true : true);
}

Try / catch

try {
  const envelope = buildQuestionResponseDeliveryEnvelope(interaction);
} catch (e) {
  if (e.message === "question_response_interaction_not_answered") {
    logger.warn({ interactionId: interaction.id }, "skipping non-answered question interaction");
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling buildQuestionResponseDeliveryEnvelope (directly or via envelope, formatDurableQuestionResponseSummary) with an interaction whose status !== 'answered', whose result is null/undefined, or whose result.cancelled === true.

Common situations: Caller resolved a question interaction id from the DB without re-checking status; a race where the interaction was cancelled between fetch and delivery; passing an interaction that only reached 'pending' or 'dismissed'; formatting a summary for an interaction that a user never answered.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/4e707c061cb752b4. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/question-response-delivery.ts:162

                  ? { placeholder: customOption.description }
                  : {}),
              },
            }
          : {}),
      };
    }),
  };
}

export function buildQuestionResponseDeliveryEnvelope(
  interaction: AskUserQuestionsInteraction,
): QuestionResponseDeliveryEnvelope {
  if (
    interaction.status !== "answered" ||
    !interaction.result ||
    interaction.result.cancelled === true
  ) {
    throw new Error("question_response_interaction_not_answered");
  }
  const questionSet = canonicalQuestionSet(interaction);
  const questionById = new Map(
    questionSet.questions.map((question) => [question.id, question]),
  );
  const response: PaperclipQuestionResponse = {
    schema: "paperclip.question_response.v1",
    answers: Object.fromEntries(
      interaction.result.answers.map((answer) => {
        const question = questionById.get(answer.questionId);
        return [
          answer.questionId,
          question?.answerMode === "text"
            ? { ...(answer.otherText ? { text: answer.otherText } : {}) }
            : {
                selectedOptionIds: answer.optionIds,
                ...(answer.otherText ? { customText: answer.otherText } : {}),
              },

View on GitHub (pinned to 3f1d897a7c)