---
title: "Approved once, emailed twice"
date: 2026-10-22
series: 27
summary: "A human approves one action once. The agent sends the email twice. Every state assertion you wrote still passes."
voice: builder
tags: [resume-idempotency, side-effect-replay, checkpoint-write-index, parallel-interrupts, durability-modes]
image: "/notes/langgraph-resume-replay/image.png"
imageAlt: null
linkedin: "urn:li:share:7518899298521194497"
sources:
  - title: "LangGraph 1.2.16 release notes (2026-10-16)"
    url: https://github.com/langchain-ai/langgraph/releases/tag/1.2.16
    date: 2026-10-16
  - title: "PR #9322, a resumed Command's update applied once (merged 2026-10-16)"
    url: https://github.com/langchain-ai/langgraph/pull/9322
    date: 2026-10-16
  - title: "PR #9315, subgraph no longer re-runs finished tasks on resume (2026-10-15)"
    url: https://github.com/langchain-ai/langgraph/pull/9315
    date: 2026-10-15
  - title: "PR #9314, resumed exit-durability writes saved once (2026-10-15)"
    url: https://github.com/langchain-ai/langgraph/pull/9314
    date: 2026-10-15
  - title: "PR #9265, an invalid resume value no longer cancels sibling tasks (2026-10-16)"
    url: https://github.com/langchain-ai/langgraph/pull/9265
    date: 2026-10-16
dateApprox: false
---

A human approves one action once. The agent sends the email twice. Every state assertion you wrote still passes.

If you run human-in-the-loop agents on LangGraph with durable execution (approval gates, interrupt(), a Postgres checkpointer), this was your production behavior until last week.

LangGraph 1.2.16 (Oct 16) fixed four resume bugs. Per the maintainers: a resumed Command applied its state update twice. A subgraph paused on parallel interrupts re-ran already-finished nodes each time a later answer arrived. Approving one tool call while another failed validation sent the approved call's email twice. In every case the final state looked correct.

The mechanism: checkpointers key pending writes by their position in a list. A resume value inserted at the front shifted the indexes, so earlier writes got saved twice and replayed on the next resume. The state converged. The email still went out twice.

What this changes: pin langgraph 1.2.16 or later, and treat every side effect that sits after an interrupt as something that will eventually run twice. Give each one an idempotency key.

The trap: your test suite asserts on final state, and final state was correct the whole time. The only test that catches this one counts the writes.

Count the side effects after interrupt() in your graphs, then count how many are idempotent. Those two numbers should match.
