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
- 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.
- Confirm the interaction is bound to the same company/issue/agent that the bridge input carries; cross-scope ids fail the lookup by design.
- If arguments changed, create a fresh tool action request (new canonical hash) and a fresh interaction instead of updating the old one.
- 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
- Always build toolAction payloads by reading the tool_action_requests row, never from client memory.
- Create a new request + interaction whenever arguments change; never mutate the hash.
- Validate company/issue/agent scoping of ids before submission.
- Watch for deleted invocations during cleanup leaving orphan interactions.
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
- github_attachment_canonical_body_mismatch
- ${label} had drifted migration history; repaired migration j
- ${label} has existing tables but no migration journal. Run m
- Database mode is postgres but no connection string was set;
- Migration journal is not a prefix of this Paperclip checkout
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/8a1d48edd33be592.
Report an issue: GitHub.