{"record":{"id":"f812640c2393067d","repo":"paperclipai/paperclip","slug":"native-interaction-governed-result-mismatch","errorCode":"native_interaction_governed_result_mismatch","errorMessage":"native_interaction_governed_result_mismatch","messagePattern":"native_interaction_governed_result_mismatch","errorType":"error_code","errorClass":"NativeInteractionBridgeError","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/native-interaction-bridge.ts","lineNumber":193,"sourceCode":"    if (!requestedIds.has(interaction.id)) continue;\n    if (interaction.companyId !== input.companyId || interaction.issueId !== input.issueId) {\n      throw new NativeInteractionBridgeError(\n        \"native_interaction_binding_mismatch\",\n        `Interaction ${interaction.id} is not bound to the native company and issue`,\n      );\n    }\n    if (interaction.kind === \"request_confirmation\" && interaction.payload.toolAction) {\n      const action = interaction.payload.toolAction;\n      if ([\"accepted\", \"rejected\"].includes(interaction.status) && (interaction.resolvedByAgentId || interaction.resolvedByRunId === input.runId)) {\n        throw new NativeInteractionBridgeError(\"native_interaction_self_approval\", \"Agents cannot resolve governed tool reviews\");\n      }\n      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)));\n      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)));\n      if (!request || !invocation || request.requestedByAgentId !== input.agentId || request.canonicalArgumentsHash !== action.argumentsHash) {\n        throw new NativeInteractionBridgeError(\"native_interaction_governed_request_unresolved\", \"Tool review has no matching authoritative invocation\");\n      }\n      if ([\"expired\", \"cancelled\"].includes(request.status)) {\n        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\");\n        responses.push({ interactionId: interaction.id, kind: interaction.kind, response: { status: interaction.status, result: structuredClone(interaction.result), executionStatus: request.status } });\n        continue;\n      }\n      if (!request.decidedByUserId || request.decidedByUserId !== interaction.resolvedByUserId || ![\"executed\", \"failed\", \"rejected\"].includes(request.status) || (request.status === \"rejected\" ? interaction.status !== \"rejected\" : interaction.status !== \"accepted\")) {\n        throw new NativeInteractionBridgeError(\"native_interaction_governed_request_unresolved\", \"Tool review must have a human decision and an authoritative terminal execution outcome\");\n      }\n      const expectedInvocationStatus = request.status === \"executed\" ? \"succeeded\" : request.status === \"rejected\" ? \"denied\" : \"failed\";\n      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\");\n    }\n    const interactionResult = record(interaction.result);\n    const supersessionOutcome = interaction.status === \"expired\"\n      && [\"superseded_by_newer_request\", \"superseded_by_comment\", \"stale_target\"].includes(String(interactionResult.outcome));\n    if (supersessionOutcome) {\n      const duplicate = interactionResult.outcome === \"superseded_by_newer_request\";\n      const decision = resolveNativeAttentionStatus({\n        facts: duplicate\n          ? {\n              companyScopeValid: true,","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/native-interaction-bridge.ts#L175-L211","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: generic accept for a request that has since expired\nresponses.push({ interactionId, kind, response: { status: \"accepted\" } });\n\n// after: carry the expiry outcome in the result\nresponses.push({ interactionId, kind, response: { status: \"accepted\", result: { toolAction: { status: \"expired\" } }, executionStatus: \"expired\" } });","handlingStrategy":"try-catch","validationCode":"const [req] = await db.select().from(toolActionRequests).where(eq(toolActionRequests.interactionId, interaction.id));\nif (req && [\"expired\", \"cancelled\"].includes(req.status)) {\n  const ok = interaction.status === req.status ||\n    (interaction.status === \"accepted\" && interaction.result?.toolAction?.status === \"expired\");\n  if (!ok) throw new Error(\"Interaction outcome does not match expired/cancelled request\");\n}","typeGuard":"function lifecycleMatches(interaction, req) {\n  if (!req) return false;\n  if (![\"expired\", \"cancelled\"].includes(req.status)) return true;\n  return interaction.status === req.status ||\n    (req.status === \"expired\" && interaction.status === \"accepted\" && interaction.result?.toolAction?.status === \"expired\");\n}","tryCatchPattern":"try {\n  await materializeNativeInteractionResponses(input);\n} catch (err) {\n  if (err instanceof NativeInteractionBridgeError && err.code === \"native_interaction_governed_result_mismatch\") {\n    // re-read request status; resubmit with matching lifecycle or mark expired\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["governance","lifecycle","state-mismatch","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}