paperclipai/paperclip · error · NativeInteractionBridgeError

native_interaction_governed_request_unresolved

native_interaction_governed_request_unresolved

Error message

native_interaction_governed_request_unresolved

What it means

For a governed `request_confirmation` interaction, the bridge verifies the tool review against its authoritative database records: a `toolActionRequests` row and a `toolInvocations` row must exist, scoped to the same company, issue, interaction, invocation, and requesting agent, and the request's `canonicalArgumentsHash` must match the hash in the payload. If any of these checks fail — no request, no invocation, wrong requesting agent, or hash mismatch — it throws `native_interaction_governed_request_unresolved`.

Source

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

  const responses: NativeInteractionResponseEnvelope[] = [];

  for (const interaction of interactions) {
    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({

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify `actionRequestId`, `invocationId`, and `argumentsHash` in the response payload match the original tool_action_request exactly; re-read the interaction from the DB and rebuild the payload rather than hand-assembling it.
  2. Confirm the interaction is bound to the same company/issue/agent that the bridge input carries; cross-scope ids fail the lookup by design.
  3. If arguments changed, create a fresh tool action request (new canonical hash) and a fresh interaction instead of updating the old one.
  4. Query `tool_action_requests` by interactionId first to confirm the authoritative record exists before attempting to materialize the response.

Example fix

// before: hand-built payload with recomputed arguments hash
const response = { toolAction: { actionRequestId, invocationId, argumentsHash: hash(newArgs) } };

// after: use the hash recorded on the authoritative request
const [request] = await db.select().from(toolActionRequests)
  .where(eq(toolActionRequests.interactionId, interaction.id));
const response = { toolAction: { actionRequestId: request.id, invocationId: request.invocationId, argumentsHash: request.canonicalArgumentsHash } };
Defensive patterns

Strategy: validation

Validate before calling

const [req] = await db.select().from(toolActionRequests)
  .where(and(eq(toolActionRequests.interactionId, interaction.id), eq(toolActionRequests.companyId, companyId)));
if (!req || req.canonicalArgumentsHash !== action.argumentsHash || req.requestedByAgentId !== agentId) {
  throw new Error("Payload does not match the authoritative tool action request");
}

Type guard

function matchesAuthoritativeRequest(action, req) {
  return Boolean(req) && req.requestedByAgentId === action.agentId && req.canonicalArgumentsHash === action.argumentsHash;
}

Try / catch

try {
  await materializeNativeInteractionResponses(input);
} catch (err) {
  if (err instanceof NativeInteractionBridgeError && err.code === "native_interaction_governed_request_unresolved") {
    // rebuild payload from the DB row; do not blind-retry with the same ids
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `materializeNativeInteractionResponses` with a toolAction response where: (1) `action.actionRequestId` or `action.invocationId` references no existing row; (2) the request/invocation belongs to a different company, issue, interaction, or agent than `input.agentId`; (3) `action.argumentsHash` differs from `request.canonicalArgumentsHash` (arguments were mutated after the request was created).

Common situations: The client cached a stale interaction payload after the request was re-created with new canonical arguments; a cross-issue or cross-agent id was pasted into the response payload; the invocation was deleted during cleanup while the interaction remained; an agent forged or guessed an actionRequestId for another agent's invocation.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/8a1d48edd33be592. Report an issue: GitHub.