paperclipai/paperclip · error · NativeInteractionBridgeError

native_interaction_governed_result_mismatch

native_interaction_governed_result_mismatch

Error message

native_interaction_governed_result_mismatch

What it means

When the underlying `toolActionRequests` row has reached a terminal lifecycle state of "expired" or "cancelled", the bridge requires the interaction's recorded outcome to match that state: the interaction status must equal the request status, except an interaction accepted with `result.toolAction.status === "expired"` is tolerated for expired requests. A mismatch throws `native_interaction_governed_result_mismatch` — the tool review's recorded lifecycle does not line up with its request.

Source

Thrown at server/src/services/native-runtime/native-interaction-bridge.ts:193

    if (!requestedIds.has(interaction.id)) continue;
    if (interaction.companyId !== input.companyId || interaction.issueId !== input.issueId) {
      throw new NativeInteractionBridgeError(
        "native_interaction_binding_mismatch",
        `Interaction ${interaction.id} is not bound to the native company and issue`,
      );
    }
    if (interaction.kind === "request_confirmation" && interaction.payload.toolAction) {
      const action = interaction.payload.toolAction;
      if (["accepted", "rejected"].includes(interaction.status) && (interaction.resolvedByAgentId || interaction.resolvedByRunId === input.runId)) {
        throw new NativeInteractionBridgeError("native_interaction_self_approval", "Agents cannot resolve governed tool reviews");
      }
      const [request] = await input.db.select().from(toolActionRequests).where(and(eq(toolActionRequests.id, action.actionRequestId), eq(toolActionRequests.companyId, input.companyId), eq(toolActionRequests.issueId, input.issueId), eq(toolActionRequests.interactionId, interaction.id), eq(toolActionRequests.invocationId, action.invocationId)));
      const [invocation] = await input.db.select().from(toolInvocations).where(and(eq(toolInvocations.id, action.invocationId), eq(toolInvocations.companyId, input.companyId), eq(toolInvocations.issueId, input.issueId), eq(toolInvocations.agentId, input.agentId)));
      if (!request || !invocation || request.requestedByAgentId !== input.agentId || request.canonicalArgumentsHash !== action.argumentsHash) {
        throw new NativeInteractionBridgeError("native_interaction_governed_request_unresolved", "Tool review has no matching authoritative invocation");
      }
      if (["expired", "cancelled"].includes(request.status)) {
        if (interaction.status !== request.status && !(interaction.status === "accepted" && interaction.result?.toolAction?.status === "expired")) throw new NativeInteractionBridgeError("native_interaction_governed_result_mismatch", "Tool review lifecycle does not match its request");
        responses.push({ interactionId: interaction.id, kind: interaction.kind, response: { status: interaction.status, result: structuredClone(interaction.result), executionStatus: request.status } });
        continue;
      }
      if (!request.decidedByUserId || request.decidedByUserId !== interaction.resolvedByUserId || !["executed", "failed", "rejected"].includes(request.status) || (request.status === "rejected" ? interaction.status !== "rejected" : interaction.status !== "accepted")) {
        throw new NativeInteractionBridgeError("native_interaction_governed_request_unresolved", "Tool review must have a human decision and an authoritative terminal execution outcome");
      }
      const expectedInvocationStatus = request.status === "executed" ? "succeeded" : request.status === "rejected" ? "denied" : "failed";
      if (invocation.status !== expectedInvocationStatus || (request.status !== "rejected" && interaction.result?.toolAction?.status !== request.status)) throw new NativeInteractionBridgeError("native_interaction_governed_result_mismatch", "Tool review outcome does not match its invocation");
    }
    const interactionResult = record(interaction.result);
    const supersessionOutcome = interaction.status === "expired"
      && ["superseded_by_newer_request", "superseded_by_comment", "stale_target"].includes(String(interactionResult.outcome));
    if (supersessionOutcome) {
      const duplicate = interactionResult.outcome === "superseded_by_newer_request";
      const decision = resolveNativeAttentionStatus({
        facts: duplicate
          ? {
              companyScopeValid: true,

View on GitHub (pinned to 01ad858492)

Solutions

  1. Before materializing, re-read the request status; if it is expired/cancelled, submit the interaction with the matching status (or include result.toolAction.status = "expired" for an accepted-but-expired outcome).
  2. Treat expired/cancelled requests as terminal: skip re-submission and inform the agent the tool action is no longer executable instead of finalizing a success.
  3. Refresh the interaction payload after any operator cancellation or supersession so the client's status reflects the current request lifecycle.
  4. Make resubmission logic idempotent: if the interaction already records expiry, do not send a fresh accept/reject.

Example fix

// before: generic accept for a request that has since expired
responses.push({ interactionId, kind, response: { status: "accepted" } });

// after: carry the expiry outcome in the result
responses.push({ interactionId, kind, response: { status: "accepted", result: { toolAction: { status: "expired" } }, executionStatus: "expired" } });
Defensive patterns

Strategy: try-catch

Validate before calling

const [req] = await db.select().from(toolActionRequests).where(eq(toolActionRequests.interactionId, interaction.id));
if (req && ["expired", "cancelled"].includes(req.status)) {
  const ok = interaction.status === req.status ||
    (interaction.status === "accepted" && interaction.result?.toolAction?.status === "expired");
  if (!ok) throw new Error("Interaction outcome does not match expired/cancelled request");
}

Type guard

function lifecycleMatches(interaction, req) {
  if (!req) return false;
  if (!["expired", "cancelled"].includes(req.status)) return true;
  return interaction.status === req.status ||
    (req.status === "expired" && interaction.status === "accepted" && interaction.result?.toolAction?.status === "expired");
}

Try / catch

try {
  await materializeNativeInteractionResponses(input);
} catch (err) {
  if (err instanceof NativeInteractionBridgeError && err.code === "native_interaction_governed_result_mismatch") {
    // re-read request status; resubmit with matching lifecycle or mark expired
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `materializeNativeInteractionResponses` for an interaction whose tool action request is "expired" or "cancelled" while the interaction claims an inconsistent status, e.g. interaction.status "accepted" without `result.toolAction.status === "expired"` against an expired request, or interaction.status "rejected" against a cancelled request.

Common situations: The interaction sat unresolved past its TTL; the request expired but a client then submitted a generic "accepted" response without the expiry marker in result.toolAction.status; a race where the request was cancelled (by supersession or operator) after the client built its accepted response; replaying an old accepted response after the request lifecycle advanced.

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@01ad858492 (2026-09-10). Data as JSON: /api/errors/f812640c2393067d. Report an issue: GitHub.