{"record":{"id":"e31a26f6219ff116","repo":"langchain-ai/deepagents","slug":"hook-resume-invocation-id-mismatch-expected-invo","errorCode":null,"errorMessage":"Hook resume invocation_id mismatch: expected {invocation_id}, got {response.invocation_id}","messagePattern":"Hook resume invocation_id mismatch: expected (.+?), got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/hooks/interrupt.py","lineNumber":107,"sourceCode":"\n    Args:\n        value: Resume payload returned by the client.\n        invocation_id: Expected invocation id from the request.\n        snapshot_id: Expected configuration snapshot id.\n\n    Returns:\n        Validated response.\n\n    Raises:\n        ValueError: If the resume payload is missing, mistyped, or mismatched.\n    \"\"\"\n    response = HOOK_INVOCATION_RESPONSE_ADAPTER.validate_python(value)\n    if response.invocation_id != invocation_id:\n        msg = (\n            f\"Hook resume invocation_id mismatch: expected {invocation_id}, \"\n            f\"got {response.invocation_id}\"\n        )\n        raise ValueError(msg)\n    if response.snapshot_id != snapshot_id:\n        msg = (\n            f\"Hook resume snapshot_id mismatch: expected {snapshot_id}, \"\n            f\"got {response.snapshot_id}\"\n        )\n        raise ValueError(msg)\n    return response\n\n\ndef is_hook_interrupt_payload(value: object) -> bool:\n    \"\"\"Return whether `value` looks like a Hooks v2 invocation interrupt.\"\"\"\n    return (\n        isinstance(value, dict) and value.get(\"type\") == HOOK_INVOCATION_INTERRUPT_TYPE\n    )\n","sourceCodeStart":89,"sourceCodeEnd":122,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/hooks/interrupt.py#L89-L122","documentation":"parse_hook_resume_value validates that a hook resume payload (submitted when the user resolves a hook interrupt) carries the same invocation_id as the suspended invocation. The pydantic HOOK_INVOCATION_RESPONSE_ADAPTER first parses the raw value; then this check ensures the response corresponds to the invocation being resumed. A mismatch means the resume value belongs to a different hook invocation and is rejected with ValueError.","triggerScenarios":"Resuming a hook interrupt via _invoke_hook/parse_hook_resume_value with a payload whose invocation_id differs from the expected one — e.g. resuming an old interrupt after the agent re-issued a new hook invocation, or mixing up interrupt IDs when several hooks are pending.","commonSituations":"Stale UI state in the coding agent (TUI) submitting an outdated resume after re-render; manually replaying saved resume payloads across sessions; concurrent hook interrupts where the caller pairs the wrong payload with the wrong invocation.","solutions":["Re-read the pending interrupt payload and resume with its exact invocation_id (and snapshot_id)","Discard stale resume values and re-request the interrupt — the old invocation_id is no longer valid","If multiple interrupts are pending, key resume payloads by interrupt id so the right value reaches the right invocation","Check for framework updates if interrupt IDs are regenerated on retry, and upgrade to a version that preserves them"],"exampleFix":"// before\nresume_value = saved_resume  # from an earlier session\nresult = parse_hook_resume_value(saved_resume, expected_invocation_id, snapshot_id)\n\n// after\npending = engine.get_pending_interrupts()[expected_invocation_id]\nresume_value = pending.build_resume(user_input)\nresult = parse_hook_resume_value(resume_value, expected_invocation_id, snapshot_id)","handlingStrategy":"validation","validationCode":"from deepagents_code.hooks.interrupt import parse_hook_resume_value\n\n# verify pairing before resuming\nif resume_payload.get(\"invocation_id\") != expected_invocation_id:\n    raise ValueError(\"stale resume payload: refetch pending interrupt\")\n","typeGuard":"def matches_invocation(payload: dict, invocation_id: str) -> bool:\n    return payload.get(\"invocation_id\") == invocation_id\n","tryCatchPattern":"try:\n    resume = parse_hook_resume_value(payload, invocation_id, snapshot_id)\nexcept ValueError as exc:\n    logger.error(\"hook resume rejected: %s\", exc)\n    resume = refetch_and_resume(invocation_id)  # re-answer the fresh interrupt\n","preventionTips":["Always resume from the payload currently stored in the pending-interrupts map","Never cache resume values across graph re-invocations or sessions","Key stored resume values by invocation_id to avoid cross-pairing multiple pending hooks"],"tags":["hooks","interrupt","resume","identity-check"],"backgroundTag":"hook-resume-id-mismatch","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}