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
- 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).
- 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.
- Refresh the interaction payload after any operator cancellation or supersession so the client's status reflects the current request lifecycle.
- 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
- Re-read request status immediately before materializing to avoid expiry/cancel races.
- For accepted-but-expired outcomes, always set result.toolAction.status to "expired".
- Refresh client payloads after operator cancellation or supersession.
- Make resubmission idempotent for interactions already recording expiry.
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
- ACPX provider ownership admission is already active
- capability_live_attempt_not_running
- Capability live turn admission was abandoned during teardown
- Chat SDK endpoint runtime was retired
- Chat SDK runtime is shutting down
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/f812640c2393067d.
Report an issue: GitHub.